skill-doctor:用真实对话日志给Agent技能做体检
很多做 Agent 开发的团队都会遇到一种尴尬skill 写完了跑通了一个手工测试用例然后就上线了。可一到真实对话里问题一个接一个冒出来——Agent 该调用的时候不调用不该调用的时候乱调用传参传得牛头不对马嘴用户只能一遍遍纠正。最可怕的是这些问题不会出现在你精心准备的那几个测试用例里它们藏在成百上千条真实对话中间。这件事的本质是大多数 skill 缺少一套“体检机制”。我们会对代码做 review会对接口做回归测试却很少像检查身体一样定期检查一套 skill 在真实对话里的调用率、失败率和用户纠正情况。而 skill 恰恰是最需要这种反馈的它的效果好不好不取决于你把它写得有多长、多全面而取决于它放在真实的 Agent 工作流里能不能被准确调用、稳定执行、有效产出。skill-doctor 的思路就是解决这个问题的不要靠人肉翻日志让 Agent 自己把最近一段时间比如 45 天的对话记录重新过一遍扮演一个“体检医生”专门检查这套 skill 写得怎么样然后产出一份可以执行的诊断报告。读完这篇文章你会知道怎么整理对话日志怎么设计 skill 的诊断维度怎么搭建一个最小可用的 skill-doctor 原型也会知道怎么防止诊断 Agent 自己胡说八道。1. 先搞清楚Agent、Skill 和“写得烂”分别指什么先说概念边界。Agent 和 Skill 是两个容易混淆的词。Agent 是一个具备感知、决策和行动能力的 AI 程序它拿着大模型当“大脑”在用户的目标驱动下规划步骤、调用工具、使用记忆最终完成任务。Skill 则是 Agent 可以调用的一种能力封装它通常包含一段提示词、可能携带工具参数模板、输入输出约束甚至是一段可执行逻辑。可以这样理解Agent 是那个“人”Skill 是它掌握的“技能”。遇到一个任务时Agent 需要先判断“这时候该用哪个技能”然后才能执行。很多开发者在这一点上就开始犯错了。他们把 Skill 写成了一个大而全的“百科全书”把所有可能用到的情况、所有边界条件、所有注意事项全部塞进提示词里。结果就是Skill 描述太泛Agent 根本判断不出来“现在该不该用它”Skill 内部逻辑太重一次调用占掉大量上下文反而挤压了对话信息的空间Skill 的触发条件写得含糊用户随口说一句擦边的话Agent 就错误地调用了它。这就是“技能写得很烂”的典型表现。总结起来可以从三个层面观察调用面不对Agent 在该调用某个 skill 时没有调用或者在不该调用时乱调用。执行不稳定skill 内部参数抽取失败、工具调用报错、输出格式解析不了。产出无效用户调用完 skill 之后还在继续追加纠正指令甚至直接放弃、反复重试。这三类问题在开发阶段极难被发现。原因很简单测试话术是你自己写的你天然知道该让 Agent 调用哪个 skill也知道答案大概长什么样。这相当于给 skill 出了一套开卷考试。而真实对话是闭卷考试用户不会按照你的假设提问。传统的 skill review 方式无非是打开 skill 文件把提示词重新读一遍凭感觉判断哪里可能有问题。这种静态审查只能发现语法错误和表述别扭发现不了“这个 skill 在真实对话里根本没被调用”这种致命问题。你可能需要换个思路让它到真实的战场里去检测而不是在会议室里模拟演练。2. skill-doctor 的核心逻辑让对话日志变成 skill 的体检样本skill-doctor 的核心思想是把“真实对话记录”当作 skill 的体检样本。假设你的 Agent 已经上线运行了 45 天这期间产生了大量用户和 Agent 的对话。这些对话是极端宝贵的资源因为它们记录了 skill 在未经修饰的真实场景下到底表现如何。用户后续的纠正是天然的反馈标注Agent 的误调用和漏调用是天然的 bug 样本工具调用失败的记录是天然的稳定性报告。skill-doctor 要做的事就是把这段时间的对话记录重新交给一个诊断 Agent让它逐段检查对照 skill 的定义来判断刚才那段对话里发生了什么Agent 该不该调用这个 skill调用后有没有出错用户有没有纠正然后把这些观察汇总成一份结构化的诊断报告。这个“让 agent 翻 45 天对话给自己体检”的设计妙在两个地方。第一它把测试集从“人工编写的用例”变成了“真实历史对话”。人工用例的作用是验证预期行为但真实对话的作用是发现意外行为。只有让 skill 面对真实用户花样百出的表达你才会知道它的触发条件是不是足够鲁棒。第二它把评估者从“开发者本人”换成了“另一个角色的 Agent”。开发者对自己的 skill 有天然的感情也很容易陷入“我当时是这么设计的所以逻辑没错”的思维惯性。诊断 Agent 没有这种包袱它只负责看对话里实际发生了什么然后机械地给出结构化结论。这就相当于你不再自己给自己看病而是让一个医生拿着你的体检数据做判断。为什么选择 45 天作为回看窗口这不是什么玄学而是覆盖面与时效性的折中。窗口太短比如 7 天可能会漏掉一部分低频 skill 的调用记录窗口太长比如 90 天对话场景可能已经漂移诊断的时效性变差。45 天能够覆盖绝大多数日常型 skill 的多次完整调用又不至于让数据规模膨胀到难以处理。如果你的 Agent 上线时间还不满 45 天那就以实际上线天数为准关键是样本要足够多。还需要强调一点这个“诊断 Agent”和“业务 Agent”最好是两个上下文相互隔离的流程。不要在用户正在参与的对话里让它做体检而是离线后把历史记录抽出来批量喂给诊断流程。这样既不会拖慢线上响应也避免诊断逻辑污染用户的真实会话。3. 体检要量化哪些指标诊断维度设计既然要给 skill 体检就不能只拿一段提示词让大模型“点评一下”。没有量化指标诊断就变成了玄学。skill-doctor 的产出应该是结构化的指标项最好能落到数字上。在看一组具体指标前先建立一个判断一套合格的 skill至少要满足“入口准、执行稳、产出有效”三个条件。所有指标都应该围绕这三个条件展开。下面是我建议的指标项。指标含义健康标准出现问题的信号调用次数统计周期内该 skill 被触发执行的总次数与业务预期基本一致很多次数/极少次数都值得关注漏调用次数根据语义判断本应调用却未调用趋近于 0Agent 把 skill 内容当成普通聊天复述误调用次数不该调用时却调用了趋近于 0用户问天气Agent 调用订单查询 skill参数抽取失败率skill 需要从对话中抽取参数抽取失败的占比越低越好最好低于 5%用户说了明确的需求Agent 却没提取到工具调用失败率skill 内部调用了工具工具报错或返回异常的比例越低越好频繁出现超时、鉴权失败、字段缺失用户纠正次数调用后用户在后续消息中纠正 Agent绝大多数调用后没有纠正单次调用后跟着 2 条以上纠偏消息输出格式达标率输出是否符合 skill 定义的 JSON/文本格式接近 100%下游处理 JSON 解析失败平均上下文占用该 skill 被调用时平均消耗多少 token能用更少的 token 解决问题时不应超大skill 提示词长到挤占对话窗口这 8 个指标并不需要全部一次性实现。你可以根据自己的业务情况先从 3 个最关键的入手调用次数、参数抽取失败率、用户纠正次数。这三个指标最容易从对话日志中统计而且能直接暴露最严重的 skill 问题。只看指标还不够。单看“客户端找不到路由频繁调用订单接口”并不能说明是 skill 写坏了还是产品只暴露了这一个入口。所以每一个指标异常背后都要让诊断 Agent 给出对应的对话片段 ID方便开发者回看原始上下文。指标是从数据里来的但最终判断仍然需要结合真实语境。在设计诊断维度的时候还有一个容易忽略的角度skill 的命名和描述质量。诊断 Agent 可以特别关注那些“描述与内容不一致”的情况。比如一个 skill 描述里写着“用于回答用户关于退货政策的疑问”但实际内容是用来处理售后审批的这就容易造成误调用。这一类问题通常不会在编译或语法层面暴露只有在大量真实对话中才会显现。4. 准备体检数据对话日志怎么组织skill-doctor 能不能跑起来很大程度上取决于你手头有没有结构化的对话日志。如果你的 Agent 是在没有日志的情况下跑了一个多月那第一步不是写诊断脚本而是先补日志。一份能用于体检的对话日志至少需要包含下面几个字段。{ conversation_id: conv_20250115_0001, turn_id: 23, timestamp: 2025-01-15T14:23:11Z, role: assistant, content: 已为您查询到订单状态目前正在配送中。, called_skill: order_query, skill_version: v3.2.0, skill_input: { order_id: SO-TS-882 }, skill_output: { status: shipping, eta: 2025-01-16T18:00:00Z }, status: ok, user_correction: false }实际上你并不需要强制让日志完整记录所有这些字段才能开始。如果你只有一个对话记录也可以通过规则大致补全。比如已知 Agent 输出“已为您查询到订单状态”就可以反推它大概率调用了订单查询 skill。但这种反推属于事后猜测准确率有限。更推荐的做法是在 Agent 的执行框架中显式埋点每次调用 skill 之前写一条日志记录被调用的 skill 名和传入参数调用结束后再写一条日志记录返回结果和执行状态。如果用的是主流 Agent 框架通常都有 trace 或中间产物机制。Skill 的调用记录、LLM 的 token 消耗、工具链的执行轨迹一般都能从 trace 里提取。如果没有现成的链路追踪能力至少要保证把“用户输入、Agent 输出、被调用的 skill、skill 的输入输出、执行状态”这五样东西写入日志。另外因为这里的原始数据是用户隐私的高风险区所以在把日志送入诊断 Agent 之前必须做脱敏处理。手机号、姓名、订单号、地址等字段要用占位符替换。这个步骤不是建议而是底线。一个能看到的做法是在写入日志时直接只记录脱敏后的简写不要等到体检时才临时清洗。45 天的对话日志可能非常大。如果全部塞给大模型做体检上下文放不下成本也扛不住。因此通常要先做一轮数据预处理按 conversation_id 分组、过滤掉无 skill 调用的纯聊天会话、再按 skill 名聚合统计基础指标。最后对于每份需要细看的对话片段截断到 skill 调用前后各 2 到 3 轮形成“诊断窗口”。这样既保留了上下文又不会把整个长对话全部吞进模型。5. skill-doctor 最小实现Python 原型这一节我们动手写一个最小可用的 skill-doctor。为了方便理解我把整个流程分成三步预处理日志、构造诊断提示词、调用大模型生成报告。假设你已经把对话日志整理成一个 JSONL 文件每行是一条消息记录字段结构沿用上一节的 schema。下面的 Python 脚本会读取这个文件按 skill 聚合统计基础指标。# 文件路径skill_doctor/prepare_replay.py import json from collections import defaultdict from datetime import datetime def load_conversations(path: str): 读取 JSONL 格式的对话日志按 conversation_id 分组。 conversations defaultdict(list) with open(path, r, encodingutf-8) as f: for line in f: line line.strip() if not line: continue record json.loads(line) conversations[record[conversation_id]].append(record) return conversations def build_skill_replay(conversations, skill_name: str, window: int 3): 提取某个 skill 的所有调用片段每条包含调用前后 window 轮上下文。 replay_items [] for conv_id, turns in conversations.items(): turns sorted(turns, keylambda x: x[turn_id]) for idx, turn in enumerate(turns): if turn.get(called_skill) ! skill_name: continue start max(0, idx - window) end min(len(turns), idx window 1) replay_items.append({ conversation_id: conv_id, turn_id: turn[turn_id], context: turns[start:end], }) return replay_items def compute_metrics(replay_items): 统计最基础的三个指标调用次数、成功次数、用户纠正相关信号。 total len(replay_items) success sum(1 for item in replay_items if any(status in t and t.get(status) ok for t in item[context])) user_followup_correction 0 for item in replay_items: context item[context] assistant_idx None for i, t in enumerate(context): if t.get(role) assistant and t.get(called_skill): assistant_idx i break if assistant_idx is not None: for t in context[assistant_idx 1:]: if t.get(role) user: user_followup_correction 1 return { total_calls: total, success_calls: success, user_followup_correction: user_followup_correction, } if __name__ __main__: convs load_conversations(replay_logs.jsonl) items build_skill_replay(convs, skill_nameorder_query) metrics compute_metrics(items) print(json.dumps(metrics, ensure_asciiFalse, indent2))这段代码做了三件事按 conversation_id 分组对话把某个 skill 的每次调用连同前后几轮上下文切出来统计调用次数、成功次数以及用户后续纠正的条数。这里的“用户后续纠正”用了一个比较粗的信号只要在 skill 调用后还有用户消息就计数一次。真实场景里你可能需要更精细的语义判断但作为最小原型这个信号足以及时暴露出很多问题。接下来是核心环节设计诊断 Agent 的提示词。这里不建议直接用一句话“请评价这个 skill 写得好不好”因为这种指令会让大模型给出泛泛而谈的表扬或批评。更好的做法是要求它忠实于给定的对话片段一个点一个点地检查只输出结构化 JSON。# 文件路径skill_doctor/diagnose.py diagnose_prompt_template 你是一位 skill-doctor负责审查 AI Agent 的技能定义。 你会收到 1. 一个 skill 的定义包括名称、描述、触发条件和提示词内容。 2. 若干段真实对话片段每段都来自用户与该 Agent 的历史交互。 你的任务 - 逐段检查对话中 Agent 是否在合适的时机调用了该 skill。 - 如果调用了检查输入参数是否从用户消息中正确抽取。 - 检查 skill 调用后的回复是否满足用户意图用户是否继续纠正。 - 不要编写虚构案例只能基于给定的对话片段作出判断。 输出格式必须是 JSON不要输出任何解释性文字。 { skill_name: order_query, findings: [ { type: missed_call|wrong_call|param_error|user_correction|ok, conversation_id: 对话ID, turn_id: 回合ID, evidence: 从对话摘录的原文证据, suggestion: 针对该问题的改进建议 } ], summary: { total_checked: 0, issue_count: 0, health_level: good|warning|bad } } def build_messages(skill_definition: dict, replay_items: list): user_content skill 定义如下\n json.dumps(skill_definition, ensure_asciiFalse, indent2) user_content \n\n对话片段如下\n json.dumps(replay_items, ensure_asciiFalse, indent2) return [ {role: system, content: diagnose_prompt_template}, {role: user, content: user_content}, ] def call_llm(messages: list) - str: # 这里替换成你实际使用的 LLM SDK # 例如 OpenAI / Anthropic / 国内模型只要支持 messages 格式即可 # response openai.ChatCompletion.create(model..., messagesmessages, response_format{type: json_object}) # return response.choices[0].message.content raise NotImplementedError(请在真实环境中接入你自己的 LLM 调用) def run_diagnosis(skill_definition, replay_items, max_items: int 50): sampled replay_items[:max_items] messages build_messages(skill_definition, sampled) raw call_llm(messages) return json.loads(raw)这段代码把诊断 Agent 的定义封装成一个模板。需要注意我刻意在系统提示词里加了“不要编写虚构案例只能基于给定的对话片段作出判断”。这是因为大模型在没有约束的情况下很容易为了凑结构而补充一些并不存在的“用户投诉”这是诊断场景里最要命的幻觉。最后你需要把它串起来写成一个入口脚本。由于真实的大模型调用和本地框架强相关这里只给出一个伪代码级别的入口函数读者可以把它替换成自己的模型调用。# 文件路径skill_doctor/main.py import json from prepare_replay import load_conversations, build_skill_replay from diagnose import run_diagnosis if __name__ __main__: skill_definition { name: order_query, description: 根据用户提供的订单号查询订单状态, trigger_condition: 用户明确询问订单状态、物流进度或配送时间, input_schema: { order_id: string, 用户提供的订单号 }, prompt: 你是订单查询助手先确认用户订单号再调用查询接口... } conversations load_conversations(replay_logs.jsonl) replay_items build_skill_replay(conversations, skill_nameorder_query) report run_diagnosis(skill_definition, replay_items, max_items30) with open(diagnosis_report.json, w, encodingutf-8) as f: json.dump(report, f, ensure_asciiFalse, indent2) print(诊断完成报告已写入 diagnosis_report.json)到这里一个最小可用的 skill-doctor 原型就跑通了。它的核心结构并不复杂读日志切样本拼提示词请求模型存报告。真正决定它有没有用的是你是否愿意在上线前和上线后都坚持跑这个流程。6. 跑一次体检从一份 45 天样本到诊断报告假设你已经准备好了 45 天的对话日志现在按照上一节的入口脚本运行一次。预期你会得到一份类似下面的报告。{ skill_name: order_query, findings: [ { type: missed_call, conversation_id: conv_20250201_0088, turn_id: 12, evidence: 用户我要退掉昨天买的那个手机。Agent请提供订单号。用户订单号是 SO-7788。Agent已记录您的退货申请请等待审核。, suggestion: 用户已经提供订单号且请求实际是退货但 Agent 没有调用 order_query导致没有核验订单信息直接进入退货流程。建议把触发条件从查询订单状态扩大到涉及订单信息的任何操作或在退货流程前强制调用订单校验。 }, { type: param_error, conversation_id: conv_20250120_0032, turn_id: 44, evidence: 用户我上周买的那双42码的鞋到了没Agent请提供订单号。, suggestion: 用户提供了商品特征和时间描述但订单号并不在原文中。skill 的参数抽取逻辑过于依赖显式订单号缺少从用户描述中关联历史订单的能力。可考虑在调用前增加一轮订单候选确认。 }, { type: wrong_call, conversation_id: conv_20250210_0121, turn_id: 7, evidence: 用户怎么客服电话打不通Agent已为您查询订单状态您的包裹正在派送中。, suggestion: 用户咨询客服联系方式并未询问订单状态Agent 却调用了 order_query。触发条件中的配送时间相关描述被过度泛化需要收紧。 } ], summary: { total_checked: 30, issue_count: 10, health_level: warning } }拿到报告后不要被“warning”这个标签牵着走。先把 findings 里每一项都点开回到对应 conversation_id 去读原始上下文。看一次真实的对话胜过读十句模型给出的“改进建议”。这里有一个解读报告的技巧把 findings 按照 type 分组统计。如果结构里大量出现 missed_call说明 skill 的触发条件过窄如果大量出现 wrong_call说明触发条件过宽如果大量出现 param_error说明输入 schema 的设计和用户真实表达脱节如果大量出现 user_correction说明 skill 的执行结果本身没有满足用户预期。这三种情况对应的治疗手段完全不一样。触发条件过窄你要扩充描述触发条件过宽你要加约束、加负面示例参数抽取脱节你要重新设计输入 schema甚至给 skill 增加一个“追问环节”。如果你只是笼统地看到“有很多问题”就开始改提示词很可能改完以后问题数量不减反增。这份报告本身不一定要做到百分之百准确。它的价值在于帮你把 45 天里肉眼看不到的问题压缩成一个可审查的清单。毕竟让你自己去翻几百上千条对话你根本坚持不下来而一个诊断 Agent 可以。7. 常见问题与排查方法skill-doctor 在落地过程中会遇到一些高频问题。我把它们整理成一张排查表方便你在实践中直接对照。问题现象可能原因排查方式解决方案日志里没有 called_skill 字段埋点缺失未记录 skill 调用检查 Agent 执行框架的日志输出在 skill 调用入口和出口各加一行结构化日志诊断报告里出现明显不存在的“用户投诉”大模型幻觉自己补充了对话内容对照报告的 conversation_id 和原文在提示词中强调“只允许使用给定的对话片段”并开启 JSON 输出约束45 天数据量太大模型上下文放不下未做裁剪直接把全量对话喂给模型查看 request 的 token 消耗按 skill 聚合抽样按调用前后各 3 轮切分窗口某个 skill 调用次数为 0无法诊断skill 本身就很少被触发检查这个 skill 是否已经过时考虑是否下线或升级触发条件观察下个周期诊断结果不稳定同一样本每次结论不同采样随机性或模型 temperature 过高固定随机种子降低 temperature对诊断流程设置 temperature0或跑 3 次取多数投票用户隐私字段被送入模型没做脱敏直接把原始对话送入诊断检查日志清洗流程在写入日志时只保留脱敏后的简写诊断前再做一次脱敏校验诊断报告泛泛而谈没有针对具体问题提示词缺少结构化输出约束检查返回结果是否都是 JSON 格式把输出格式写死在 prompt 中并增加“必须引用原文证据”要求里面最隐蔽的问题是“调用次数为 0”。当你辛辛苦苦搞了一套诊断系统却发现某个 skill 一次都没被调用过你可能会觉得这个 skill 很健康。但真相往往相反这个 skill 可能已经名存实亡Agent 在真实场景里完全没有识别到该用它的时机。这种情况下应该去翻一翻用户对话看看是否存在“用户本来需要这个能力但 Agent 却做了别的处理”的漏调用案例。还有一个需要警惕的坑不要试图用同一个 prompt 检查所有 skill。订单查询类 skill 和内容生成类 skill它们的诊断侧重点完全不同。前者更看重参数抽取和调用时机后者更看重输出风格和事实准确性。如果你把所有 skill 都丢进同一个诊断模板报告的质量会明显下降。实际做法是根据 skill 的类型预设几套诊断提示词模板或者让诊断 Agent 先读取 skill 定义再自动决定检查重点。8. 把 skill 体检变成工程规范最佳实践skill-doctor 做出来之后不应该只是心血来潮跑一次。把它嵌入团队的学习与发布流程价值会放大很多。第一个实践建议把体检和发布绑定。每次修改 skill 版本时都强制回放最近一段时间的对话日志把“体检报告”和 skill 的新版本一起提交。如果报告中新增了严重问题就说明这次修改有问题要么回滚要么继续修。这其实就相当于给 skill 增加了一道“回放测试门禁”。因为你永远不可能靠几个手工用例覆盖真实对话的多样性而历史对话就是最接近真实分布的数据集。第二个建议给每个 skill 建一份“病历档案”。每一个版本的体检报告都保留下来形成历史的健康曲线。这样你才能回答一个关键问题上一次我改完提示词以后漏调用率到底是降了还是升了很多情况下开发者凭记忆判断“好像改完之后更好了”但翻出病历就会发现某个问题反而恶化了。第三个实践建议不要让自动报告直接代替人修改 skill。skill-doctor 的定位是体检医生它负责发现问题但治疗方案的最终决定权必须掌握在开发者和业务负责人手里。原因很简单大模型给出的“优化建议”有时候看起来有理有据但它不理解你的业务约束、不掌握历史需求很容易把 skill 改得更合适于历史对话却丢失了对未来新需求的处理能力。因此报告里的 suggestion 只能作为参考真正改代码的还得是人。第四个建议体检频率不一定要严格等到 45 天。如果对话量很大可以拆成每周跑一次“周体检”每月跑一次“月体检”。对于刚刚上线的 skill前两周最好每天跑一次增量体检因为新 skill 的触发条件最容易在这一时期暴露出问题越早发现修起来越便宜。第五个建议把诊断 Agent 的每一次判断也当作数据收集。这个诊断 Agent 本身也是一个 Agent它的判断质量需要被评估。你可以定期抽查报告看它给出的 missed_call 判断到底准不准把误判反馈到诊断 prompt 里。这样skill-doctor 会随着使用次数的增加而越来越像这个团队自己的“资深审查员”。9. 总结skill 不是写完的而是“养”出来的这篇文章真正想说明白的一件事就是 skill 的质量不能靠写的时候自我感觉良好也不能靠几组手工测试用例来保证。它需要在真实对话中被反复检验逐步调整这个过程就是“养” skill。skill-doctor 的存在意义是把这个养的过程从人肉翻日志变成有数据、有指标、有报告的结构化流程。你现在已经知道了核心思路对话日志是 skill 最好的测试集诊断 Agent 是人类开发者的体检医生量化指标是连接原始日志与改进动作的桥梁。接下来你可以先从一个小范围开始比如只挑一个最核心的 skill把它最近 30 天的对话日志抽出来按本文的 Python 原型跑一次看看它会暴露多少你完全没想到的问题。等你跑完第一轮就会发现真正有价值的不是那份报告本身而是报告把你引向的那几段原始对话。那些对话里藏着的才是 skill 优化最重要的线索。别指望一份报告能直接帮你改好技能但它能帮你把目光从“我该怎么写提示词”挪到“用户实际上在怎么和 Agent 互动”上。这个挪动比任何技巧都管用。

相关新闻

最新新闻

日新闻

周新闻

月新闻