多轮对话状态跟踪在 Harness 中的实现:用 TaoToken 统一 Key 打通 DST 验证链路 1. 为什么要在 Harness 流水线里做多轮对话状态跟踪多轮对话状态跟踪Dialogue State TrackingDST说白了就是让对话机器人记住「上一句聊的是哪条流水线、哪个环境、哪次执行」这样用户第二轮只问「怎么修复」时系统还能接得上。它适合谁适合正在把 AI 助手塞进 DevOps 流程的团队尤其是已经在用 Harness 跑 CI/CD、想让排障对话不再反复追问上下文的工程师。我在实际项目里遇到过很典型的场景流水线挂了运维同学在 Harness 的 AI 助手里问「今天支付服务那条流水线为什么失败」助手答「单元测试报 NullPointerException」。接着他问「怎么修」助手回「请问你指的是哪条流水线」。这一下上下文全丢了用户得把项目、流水线、执行记录再报一遍。问题不在模型笨而在于中间缺了一层状态管理每一轮请求都是独立的历史信息没有被结构化地保存和传递。Harness 本身是软件交付平台它的业务对象很多Account、Org、Project、Pipeline、Execution、Environment、Service、Deployment。用户一句话里可能只说了「支付服务」但系统需要把它映射到具体的 Project 和 Pipeline还要确认这个用户有没有权限看这条流水线。如果只靠大模型自由发挥很容易编出一个不存在的流水线 ID后续回复就会把人带偏。所以 DST 在这里不是锦上添花而是让 AI 助手从「能聊天」变成「能干活」的关键一层。这一篇我不讲空泛的架构图直接交付能跑的东西用 TaoToken 统一 Key 和 API 通道接入对话模型在 Harness Pipeline 里跑通多轮状态槽位更新与断言。你会看到可复制的 pipeline YAML、环境变量配置、DST 用例配置以及本地和流水线两段验证动作。目标很明确让「第一轮问失败原因、第二轮问修复方案」这种多轮对话在流水线里可复现、可断言、可回归。核心检索词先摆出来多轮对话状态跟踪、Harness、DST、DevOps、对话状态跟踪。下面所有配置都围绕这几个词展开不堆砌只讲能落地的部分。2. TaoToken 统一 Key 接入环境变量与模型通道配置在 Harness 里做 DST 验证第一件麻烦事是模型通道。团队里可能有人用这个模型、有人用那个模型Key 散落在各个地方流水线一跑就报 401。TaoToken 在这里的作用是提供一个统一的 API 通道和 Key 管理入口让本地调试和 Harness 流水线用同一套 Base URL 和 Key减少「本地能跑、流水线挂掉」的扯皮。先明确三个东西后面所有配置都围绕它们Base URLhttps://taotoken.net/apiAPI Key在控制台创建形如sk-xxxx不要写进代码仓库Model ID对话模型填你实际调用的模型标识比如gpt-4o-mini或claude-3-5-sonnet以控制台模型列表为准如果你还没有 Key可以到控制台的 API Keys 页面创建https://taotoken.net/console/api-keys?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewrite 。创建后先别急着塞进 Harness本地用 curl 验一下通道是否通。本地验证命令如下注意把$TAOTOKEN_API_KEY换成你自己的 Keyexport TAOTOKEN_API_KEYsk-你的key export TAOTOKEN_BASE_URLhttps://taotoken.net/api curl -s $TAOTOKEN_BASE_URL/v1/chat/completions \ -H Authorization: Bearer $TAOTOKEN_API_KEY \ -H Content-Type: application/json \ -d { model: gpt-4o-mini, messages: [ {role: user, content: 只回复两个字通了} ], temperature: 0 }如果返回的 JSON 里有choices[0].message.content说明通道没问题。这一步很关键因为后面 Harness 流水线里报的很多错根因都是 Key 或 Base URL 配错而不是 DST 逻辑本身。接下来在 Harness 里配置环境变量。进入 Harness 项目设置找到 Secrets 或 Environment Variables建议把 Key 放 SecretBase URL 和 Model ID 放普通变量。命名建议统一加前缀避免和别的服务冲突变量名类型示例值用途TAOTOKEN_API_KEYSecretsk-xxxx模型通道鉴权TAOTOKEN_BASE_URLStringhttps://taotoken.net/api统一 API 入口DST_MODEL_IDStringgpt-4o-mini对话模型标识DST_SESSION_TTLString86400状态过期秒数HARNESS_ACCOUNT_IDStringacc_xxx租户隔离用这里有个容易踩的坑Harness 的 Secret 在日志里会被掩码但如果你在 shell 里用echo打印可能触发掩码导致后续字符串拼接出错。所以脚本里不要打印 Key直接用变量引用。关于模型选择DST 的实体抽取和状态更新对模型要求不一样。抽取候选实体这种任务用便宜快的小模型就够状态更新需要做「保留哪些旧实体、丢弃哪些」的决策可以用稍强的模型。你可以在 TaoToken 的模型对话页面先手动试几轮确认模型对 JSON 输出的稳定性https://taotoken.net/models?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewrite 。如果模型经常在 JSON 外面包一层解释文字后面解析就会失败这时候要么换模型要么在 prompt 里强制「只返回 JSON」。如果你打算长期在 Harness 里跑 Agent 类任务比如让 AI 自动分析失败日志并给出修复建议可以了解 Coding Plan 的额度方式https://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewrite 。不过本篇的重点是 DST 验证链路先把通道和状态跑通再考虑规模化。配置完成后建议在 Harness 里加一个最简的连通性检查步骤作为流水线的第一道门。这样后面 DST 用例失败时你能快速判断是通道问题还是逻辑问题。3. 可复制的 Harness Pipeline YAML 与 DST 用例配置这一节是核心直接给可复制的配置。整体思路Harness Pipeline 里跑一个 Python 脚本脚本负责调用 TaoToken 通道执行两轮对话维护一个 DST 状态对象最后对状态里的槽位做断言。断言失败流水线就红这样多轮状态跟踪的结果就可回归。先看目录结构建议在仓库里这样放dst-harness/ ├── pipeline/ │ └── dst-verify.yaml ├── scripts/ │ └── dst_runner.py ├── config/ │ └── dst_cases.json └── requirements.txtrequirements.txt内容很简单openai1.0.0 pyyaml注意这里用的是 OpenAI 兼容 SDK通过base_url指向 TaoToken 通道不需要额外装奇怪的包。先写 DST 用例配置config/dst_cases.json。这个文件定义多轮对话的输入和期望状态是断言的依据{ cases: [ { case_id: dst_pipeline_repair, description: 第一轮问失败原因第二轮问修复方案验证流水线槽位不丢失, turns: [ { user: 今天支付服务那条 Java 流水线为什么失败, expect_slots: { service: 支付服务, pipeline_type: Java } }, { user: 怎么修复, expect_slots: { service: 支付服务, pipeline_type: Java, intent: repair } } ] } ] }这里的关键是expect_slots第一轮结束后状态里应该有service和pipeline_type第二轮用户没有重复这两个词但状态里必须还在同时新增intentrepair。这就是 DST 要验证的核心槽位跨轮保留与更新。接下来是scripts/dst_runner.py。它做四件事读用例、调模型、更新状态、断言。为了让你能直接跑我把状态更新写成「模型抽取 本地合并」的混合方式既用模型理解自然语言又用代码保证槽位不丢import json import os import sys from openai import OpenAI client OpenAI( api_keyos.environ[TAOTOKEN_API_KEY], base_urlos.environ.get(TAOTOKEN_BASE_URL, https://taotoken.net/api), ) MODEL_ID os.environ.get(DST_MODEL_ID, gpt-4o-mini) def extract_slots(user_text, history_summary): prompt f 你是 DevOps 对话状态抽取器。根据用户当前输入和历史摘要抽取槽位。 只返回 JSON格式{{slots: {{key: value}}, intent: 字符串}} 历史摘要{history_summary} 当前输入{user_text} resp client.chat.completions.create( modelMODEL_ID, messages[{role: user, content: prompt}], temperature0, response_format{type: json_object}, ) content resp.choices[0].message.content return json.loads(content) def merge_state(old_state, new_slots, new_intent): merged dict(old_state) for k, v in new_slots.items(): if v: merged[k] v if new_intent: merged[intent] new_intent return merged def run_case(case): state {} history_summary for idx, turn in enumerate(case[turns]): result extract_slots(turn[user], history_summary) state merge_state(state, result.get(slots, {}), result.get(intent)) history_summary f用户说{turn[user]}当前槽位{json.dumps(state, ensure_asciiFalse)} print(f[turn {idx1}] state{json.dumps(state, ensure_asciiFalse)}) for key, expected in turn[expect_slots].items(): actual state.get(key) if actual ! expected: print(fASSERT FAIL: case{case[case_id]} turn{idx1} key{key} expected{expected} actual{actual}) return False print(fASSERT PASS: {case[case_id]}) return True def main(): with open(config/dst_cases.json, r, encodingutf-8) as f: cases json.load(f)[cases] all_pass True for case in cases: if not run_case(case): all_pass False sys.exit(0 if all_pass else 1) if __name__ __main__: main()这段代码里response_format{type: json_object}是保证解析稳定的关键。如果模型不支持这个参数就去掉它但在 prompt 里必须强调「只返回 JSON不要任何解释」。然后是 Harness Pipeline YAML。这里用 Harness 的 CI 阶段跑一个 Run 步骤。注意把 Secret 引用写对Harness 里通常用secrets.getValue(...)的表达式pipeline: name: dst-verify-pipeline identifier: dst_verify_pipeline projectIdentifier: your_project orgIdentifier: your_org tags: {} stages: - stage: name: dst-verify identifier: dst_verify type: CI spec: cloneCodebase: true execution: steps: - step: type: Run name: run-dst-cases identifier: run_dst_cases spec: connectorRef: your_connector image: python:3.11-slim shell: Bash command: | set -e pip install -r requirements.txt export TAOTOKEN_API_KEYsecrets.getValue(taotoken_api_key) export TAOTOKEN_BASE_URLpipeline.variables.taotoken_base_url export DST_MODEL_IDpipeline.variables.dst_model_id python scripts/dst_runner.py infrastructure: type: KubernetesDirect spec: connectorRef: your_k8s_connector namespace: default variables: - name: taotoken_base_url type: String value: https://taotoken.net/api - name: dst_model_id type: String value: gpt-4o-mini如果你用的是 Harness Cloud 或者别的执行环境infrastructure那段按你实际的环境改。核心是command里的三步装依赖、注入环境变量、跑脚本。脚本退出码非 0Harness 步骤就失败流水线就红。这里要提醒一个细节Harness 的变量表达式在 YAML 里是...如果你在 shell 里用单引号包住可能不会被替换。所以export那几行不要加单引号直接写表达式。另外如果你在 Harness 里用 Cline MCP 或类似的工具做 AI 辅助记得把三件套配全Base URL、Key、Model ID。缺一个都会报连接错误。Base URL 用https://taotoken.net/apiKey 用 SecretModel ID 用你实际选的模型。这三者在本地和流水线里必须一致否则就会出现「本地通、流水线 401」的经典问题。4. 验证请求与成功结果本地与流水线两段动作配置写完了得验证。我习惯分两段先在本地把 DST 逻辑跑通再推到 Harness 流水线里跑。这样出问题时能快速定位是模型通道、脚本逻辑还是 Harness 环境的问题。本地验证第一步确认通道。前面第 2 节的 curl 已经做过这里再做一次带 JSON 输出的调用确认模型能稳定返回结构化内容export TAOTOKEN_API_KEYsk-你的key export TAOTOKEN_BASE_URLhttps://taotoken.net/api export DST_MODEL_IDgpt-4o-mini python -c from openai import OpenAI import os, json client OpenAI(api_keyos.environ[TAOTOKEN_API_KEY], base_urlos.environ[TAOTOKEN_BASE_URL]) resp client.chat.completions.create( modelos.environ[DST_MODEL_ID], messages[{role:user,content:返回JSON{\slots\:{\service\:\支付服务\},\intent\:\query\}}], temperature0, response_format{type:json_object} ) print(resp.choices[0].message.content) 如果输出是合法的 JSON说明通道和模型都 OK。如果报local proxy failed或连接超时先检查 Base URL 是不是写成了https://taotoken.net/api/v1这种重复路径正确写法是https://taotoken.net/apiSDK 会自动拼/v1/chat/completions。本地验证第二步跑 DST 用例cd dst-harness pip install -r requirements.txt python scripts/dst_runner.py期望输出类似[turn 1] state{service: 支付服务, pipeline_type: Java} [turn 2] state{service: 支付服务, pipeline_type: Java, intent: repair} ASSERT PASS: dst_pipeline_repair看到ASSERT PASS说明多轮状态跟踪在本地是通的。注意第二轮 state 里service和pipeline_type还在这就是 DST 的价值用户没说但状态记住了。本地通过后提交代码触发 Harness 流水线。在 Harness 的 Execution 页面看run-dst-cases步骤的日志。成功时你会看到同样的ASSERT PASS并且步骤状态是 Success。如果失败日志里会打印ASSERT FAIL告诉你哪个槽位对不上。这里有个实用技巧在 Harness 日志里搜state能快速看到每一轮的状态快照。如果第一轮就缺槽位说明模型抽取有问题如果第一轮有、第二轮丢了说明状态合并逻辑有问题。把这两类问题分开排查效率会高很多。流水线跑通后建议把dst_cases.json当成回归用例库。每次改 prompt 或换模型都跑一遍。DST 这种东西最怕的就是「这次好了下次换个说法又丢了」。有了断言回归就有依据。如果你在验证过程中想手动试几轮对话看看模型对槽位的理解可以到模型对话页面直接聊https://taotoken.net/models?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewrite 。手动试出来的好 prompt再固化到脚本里。5. 常见报错排查401、local proxy failed、reading choices、OAuth这一节按真实报错来。你在 Harness 里跑 DST大概率会遇到下面几类问题。我把现象、原因、处理方式列清楚方便你对照。401 Unauthorized。现象是模型调用返回 401日志里可能只写Authentication failed。原因通常是 Key 没注入、Key 过期、或者 Harness Secret 引用写错。处理先在本地用同一个 Key 跑 curl确认 Key 本身有效再检查 Harness 里TAOTOKEN_API_KEY的 Secret 名称和表达式是否一致。注意 Harness 的 Secret 在不同 scopeProject/Org/Account下名称可能冲突引用时最好带上完整路径。local proxy failed。这个报错通常出现在网络层意思是请求没到达目标地址。原因可能是 Base URL 写错、执行环境没有出网权限、或者把 Base URL 配成了带/v1的完整路径导致 SDK 拼接后变成/v1/v1/...。处理确认TAOTOKEN_BASE_URLhttps://taotoken.net/api不要加/v1确认 Harness 执行集群能访问外网如果公司网络有出口限制需要让网络同学放行。reading choices。典型报错是TypeError: Cannot read properties of undefined (reading choices)或 Python 里的KeyError: choices。这说明返回体里没有choices字段通常是请求根本没成功返回的是错误 JSON但代码直接去取choices了。处理在解析前先打印完整响应判断是不是 401 或 429。429 是限流需要降低并发或检查额度。另外如果模型返回的是流式响应而你没处理也会出现类似问题DST 场景建议先用非流式。OAuth 相关报错。如果你在 Harness 里用 OAuth 方式连接代码仓库或外部服务可能会看到OAuth token expired或invalid_grant。这跟 TaoToken 通道无关是 Harness 连接器的问题。处理到 Harness 的 Connectors 页面重新授权。注意区分模型通道用 API Key代码仓库用 OAuth两者不要混。模型返回非 JSON。现象是json.loads抛异常。原因可能是模型不支持response_format或者 prompt 不够强硬。处理去掉response_format在 prompt 末尾加「只返回 JSON不要 markdown 代码块不要解释」。如果模型还是加代码块就在解析前用正则把json 和去掉。槽位第二轮丢失。这不是报错但属于逻辑失败。原因通常是merge_state没做或者每轮都新建了 state。处理确认 state 在循环外初始化每轮用merge_state合并而不是覆盖。另外如果模型在第二轮返回了空槽位merge_state里的if v判断会跳过空值保证旧槽位不被清掉。Harness 变量没替换。现象是脚本里拿到的 Base URL 是字面量pipeline.variables.taotoken_base_url。原因是在 YAML 里用了单引号或者变量定义在错误的 scope。处理去掉单引号确认变量定义在 pipeline 级别并且表达式拼写正确。权限校验失败。如果你在 DST 里加了 Harness RBAC 校验可能会遇到 403。处理确认调用 Harness API 的 Service Account 有对应权限并且实体确实属于当前租户。DST 状态里不要存跨租户的实体这是硬性要求。排查时记住一个顺序先确认通道curl 通不通再确认脚本本地跑不跑得通最后确认 Harness 环境变量、Secret、网络。这个顺序能帮你少走很多弯路。6. 把 DST 验证链路固化到日常交付走到这里你已经有了可复制的 pipeline YAML、环境变量配置、DST 用例和两段验证动作。接下来最重要的是把它变成日常习惯而不是一次性 demo。我的做法是把dst_cases.json当成产品需求来维护。每次线上出现「AI 助手答非所问」的反馈就把它转成一条 DST 用例补上期望槽位然后跑流水线。这样 DST 的覆盖会越来越厚回归也越来越稳。另外模型和 prompt 是会变的。今天用gpt-4o-mini抽取稳定明天换个模型可能就飘。所以流水线里的断言不能省。宁可多花几分钟跑用例也不要等用户投诉了才发现状态丢了。如果你需要更细的接入文档包括不同语言的 SDK 示例和错误码说明可以看接入文档https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewrite 。里面关于 Base URL 和鉴权的部分和本篇配置是对应的。最后留一个实用技巧在 Harness 流水线里加一个「状态快照归档」步骤把每轮state的日志存成 artifact。这样当 DST 断言失败时你能直接对比历史快照看是哪个槽位在哪一轮开始漂移的。这个动作很小但排查效率提升很明显。