09 实践课-基础智能体测试与 AI 协同调试
基础智能体测试与 AI 协同调试
关联:索引
- 场景提问:同一句话“玻璃怎么分拣”,为什么有时能查到规则、有时触发不了工具?怎么证明问题来自“意图识别”还是“工具执行”?
1. 功能测试(端到端最小闭环)
目标:验证“输入→意图→路由→工具/模型→输出”是否符合预期。
用例模板(建议复制到测试记录/错例清单):
- 用例编号:
- 测试目的:
- 输入(原句):
- 期望意图:
- 期望工具(如需):
- 期望关键证据字段(rule_id/receipt/trace_id 等):
- 实际输出(粘贴关键片段):
- 结论(Pass/Fail):
- 备注(异常与复现条件):
最小功能用例(分拣场景示例):
- F-01:查询规则:输入“查一下 glass 的分拣规则”,期望触发规则查询工具,输出包含规则编号与 trace_id。
- F-02:提交反馈:输入“提交反馈 t001 失败,玻璃破损”,期望触发反馈工具,输出包含回执与 trace_id。
- F-03:普通解释:输入“为什么要先做意图识别?”,期望不触发工具或由模型直接解释,输出包含可解释理由。
实操案例(端到端功能测试:输出必须可复验):
目标:让学生把“看起来对”变成“证据链可复验”。
- 选择 1 条查询规则用例(如 F-01),运行智能体一次,记录以下证据:
- 用户输入原文
- 意图识别输出(intent + 槽位字段)
- 工具入参(item_type 或 task_id/ok/message)
- 工具输出(包含 rule_id 或 receipt + trace_id)
- 最终回复(必须引用 rule_id/receipt/trace_id)
- 工具被触发(或明确说明不需要工具)
- 最终回复引用了可复验字段(rule_id/receipt/trace_id 至少 1 个)
- 失败时能定位:输出中能看到 ERROR/NOT_FOUND 等明确类型 + trace_id
示例输出片段(格式示意,便于学生对照):
用户输入:查一下 glass 的分拣规则
解析:intent=QUERY_RULE item_type=glass | parse_trace_id=9f21a3c1
最终输出:【规则查询结果】R-GLASS-01 | 玻璃制品:轻放、防碰撞;单独箱;贴易碎标;优先人工复核。 | action=use_fragile_bin | trace_id=2b8c7d19 | parse_trace_id=9f21a3c1
基于上节课项目的最小复现(直接跑一次并粘贴输出):
终端(在项目根目录执行):
cd ".\04_sorting_agent_practice"
python -c "from app_with_intent import build_llm, build_agent, run_with_router; llm=build_llm(); agent=build_agent(llm); print(run_with_router(agent,llm,'查一下 glass 的分拣规则'))"
2. 意图识别准确率测试(用标注数据评估)
目标:用标注数据统计意图识别是否正确,并定位“最常错的那一类”。
最小标注数据格式(示例,JSON Lines):
文件:data.jsonl(建议放在 04_sorting_agent_practice/ 下,便于命令行直接读取)
{"text":"查一下 glass 的分拣规则","intent":"QUERY_RULE","item_type":"glass"}
{"text":"提交反馈 t001 FAIL 玻璃破损","intent":"SUBMIT_FEEDBACK","task_id":"t001","ok":false,"message":"玻璃破损"}
{"text":"为什么要做意图识别?","intent":"GENERAL"}
- 总体准确率:预测正确 / 总样本数
- 关键类准确率:QUERY_RULE 与 SUBMIT_FEEDBACK 的准确率要单独统计
- 错例清单:每个错例必须记录“原句、预测结果、正确标签、原因猜测、修复计划”
说明(知识点校验):
- “混淆矩阵”用于定位“最常错的类对”(例如 QUERY_RULE 被误判为 GENERAL),再针对性修正规则或澄清策略。
实操案例(用标注数据计算准确率与混淆矩阵:最小脚本示例):
使用方式:把标注数据保存为 data.jsonl,每行一个 JSON;把你的 detect_intent(text, llm) 或等价函数接进来即可。
文件建议:04_sorting_agent_practice/eval_intent_accuracy.py
import json
from collections import Counter, defaultdict
LABELS = ["QUERY_RULE", "SUBMIT_FEEDBACK", "GENERAL"]
def load_jsonl(path: str):
with open(path, "r", encoding="utf-8") as f:
for line in f:
line = line.strip()
if line:
yield json.loads(line)
def evaluate(data_path: str, predict_intent):
total = 0
correct = 0
per_label_total = Counter()
per_label_correct = Counter()
matrix = defaultdict(Counter)
for row in load_jsonl(data_path):
text = row["text"]
gold = row["intent"]
pred = predict_intent(text)
total += 1
per_label_total[gold] += 1
matrix[gold][pred] += 1
if pred == gold:
correct += 1
per_label_correct[gold] += 1
overall_acc = correct / total if total else 0.0
key_acc = {
k: (per_label_correct[k] / per_label_total[k] if per_label_total[k] else 0.0)
for k in ["QUERY_RULE", "SUBMIT_FEEDBACK"]
}
print("overall_acc:", round(overall_acc, 4), f"({correct}/{total})")
print("key_acc:", {k: round(v, 4) for k, v in key_acc.items()})
print("confusion_matrix (gold -> pred counts):")
for gold in LABELS:
row = {pred: matrix[gold][pred] for pred in LABELS}
print(gold, row)
if __name__ == "__main__":
def predict_intent_stub(text: str) -> str:
t = (text or "").strip()
if "提交反馈" in t or "FEEDBACK" in t.upper():
return "SUBMIT_FEEDBACK"
if "规则" in t or "怎么分拣" in t or "如何分拣" in t:
return "QUERY_RULE"
return "GENERAL"
evaluate("data.jsonl", predict_intent_stub)
- 输出 overall_acc + QUERY_RULE 与 SUBMIT_FEEDBACK 的准确率
- 打印混淆矩阵并从中挑出 1 个“最常错的类对”,写进错例清单与修复计划
可选加分(更贴近工程评估):
- 类别不均衡时,额外报告关键类的召回率(漏判多少)比只看准确率更有指导意义。
对接你自己的意图函数(示例):
如果你的 detect_intent 返回的是对象(如 IntentResult(intent=..., ...)),可以做一个薄封装,保证 predict_intent(text) 返回三类标签字符串即可。
文件:04_sorting_agent_practice/eval_intent_accuracy.py(放在同一个脚本中即可)
from intent import detect_intent
def predict_intent_from_result(text: str, llm) -> str:
r = detect_intent(text, llm)
return r.intent
方式 B(保留 predict_intent_stub 做对照,同时新增真实预测函数):
文件:04_sorting_agent_practice/eval_intent_accuracy.py(把脚本末尾的 __main__ 部分替换为下面这一段)
from app_with_intent import build_llm
if __name__ == "__main__":
def predict_intent_stub(text: str) -> str:
t = (text or "").strip()
if "提交反馈" in t or "FEEDBACK" in t.upper():
return "SUBMIT_FEEDBACK"
if "规则" in t or "怎么分拣" in t or "如何分拣" in t:
return "QUERY_RULE"
return "GENERAL"
llm = build_llm()
def predict_intent_real(text: str) -> str:
return predict_intent_from_result(text, llm)
print("== baseline (stub) ==")
evaluate("data.jsonl", predict_intent_stub)
print("== real (detect_intent) ==")
evaluate("data.jsonl", predict_intent_real)
与上节课项目对齐的提醒:
intent.detect_intent的“规则查询”抽取item_type默认提取英文关键字(如 glass/battery),所以标注数据里建议保留该英文关键字,避免槽位缺失导致 need_clarification。intent.detect_intent对解释类关键词(为什么/原理/区别等)会强制no_tools=true,这类样本应标注为GENERAL。
3. 工具调用测试(契约与异常路径)
目标:验证工具能被触发、入参合规、返回格式稳定,并覆盖错误分支。
- 输入:字段是否齐全、是否可校验(空值/非法值要明确报错)。
- 输出:必须包含可复验字段(rule_id/receipt/trace_id),并且格式稳定可解析。
- 异常:输入不合法/未找到规则/外部接口失败必须返回可定位信息(错误类型 + trace_id)。
最小工具测试用例(示例):
- T-01:规则查询工具入参为空 → 期望返回 ERROR 且带 trace_id。
- T-02:未知物品类型 → 期望 NOT_FOUND 且带 trace_id。
- T-03:反馈工具 task_id 为空 → 期望 ERROR 且带 trace_id。
实操案例(工具契约测试:覆盖正常 + 异常分支):
目标:让学生能区分“工具没触发”和“工具执行报错”,并能用输出字段定位。
用例:
-
T-04:query_sorting_rule(item_type="glass") → 必须返回 rule_id + action + trace_id
-
T-05:query_sorting_rule(item_type="") → 必须返回 ERROR + trace_id
-
T-06:query_sorting_rule(item_type="unknown") → 必须返回 NOT_FOUND + trace_id
-
T-07:submit_sorting_feedback(task_id="t001", ok=False, message="玻璃破损") → 必须返回 RECEIPT + trace_id
-
T-08:submit_sorting_feedback(task_id="", ok=True, message="正常") → 必须返回 ERROR + trace_id
-
工具输出必须“可解析、可定位”:包含明确类型(ERROR/NOT_FOUND/RECEIPT 等)与 trace_id
-
异常分支不能默默返回空字符串或只有一句“失败了”,否则视为契约不合格
基于上节课项目的工具直测(不经过 agent,定位更快):
终端(在项目根目录执行):
cd ".\04_sorting_agent_practice"
python -c "from tools_sorting import query_sorting_rule; print(query_sorting_rule.invoke({'item_type':'glass'}))"
python -c "from tools_sorting import query_sorting_rule; print(query_sorting_rule.invoke({'item_type':''}))"
python -c "from tools_sorting import query_sorting_rule; print(query_sorting_rule.invoke({'item_type':'unknown'}))"
python -c "from tools_sorting import submit_sorting_feedback; print(submit_sorting_feedback.invoke({'task_id':'t001','ok':False,'message':'玻璃破损'}))"
python -c "from tools_sorting import submit_sorting_feedback; print(submit_sorting_feedback.invoke({'task_id':'','ok':True,'message':'正常'}))"
实操案例(回归测试:批量跑用例并断言“证据字段”):
目标:修复后必须“一次性验证多条用例”,避免只修好了一个例子却引入新问题。
文件建议:04_sorting_agent_practice/regression_test.py
import re
from dataclasses import dataclass
from typing import List
@dataclass
class Case:
case_id: str
text: str
must_contain: List[str]
TRACE_RE = re.compile(r"\btrace_id=([0-9a-fA-F]{6,})\b")
def has_evidence(text: str) -> bool:
if not text:
return False
if "R-" in text:
return True
if "RECEIPT:" in text:
return True
if TRACE_RE.search(text):
return True
return False
def run_regression(cases: List[Case], invoke_text) -> None:
failed = []
for c in cases:
final_text = invoke_text(c.text) or ""
ok_contains = all(x in final_text for x in c.must_contain)
ok_evidence = has_evidence(final_text)
if not (ok_contains and ok_evidence):
failed.append((c.case_id, final_text[:160]))
if failed:
raise RuntimeError(f"regression_failed: {failed}")
if __name__ == "__main__":
cases = [
Case("R-01", "查一下 glass 的分拣规则", ["【规则查询结果】", "R-GLASS-01", "parse_trace_id="]),
Case("R-02", "提交反馈 t001 失败,玻璃破损", ["【反馈回执】", "RECEIPT:", "task_id=t001", "parse_trace_id="]),
Case("R-03", "为什么要先做意图识别?", ["【解释】", "no_tools=true", "parse_trace_id="]),
Case("R-04", "", ["【需要澄清】", "parse_trace_id="]),
]
from app_with_intent import build_llm, build_agent, run_with_router
llm = build_llm()
agent = build_agent(llm)
def invoke_text(text: str) -> str:
return run_with_router(agent, llm, text)
run_regression(cases, invoke_text)
1)严格口径(更工程化):只把“执行证据”算作 evidence
- 适用:你要求最终回复必须引用工具证据(rule_id/receipt/trace_id),不接受只有解析/路由信息的输出。
- 现状:脚本默认就是这种口径(
R-/RECEIPT:/trace_id=)。
2)宽松口径(更适合解释/澄清分支):把 parse_trace_id= 也算作 evidence
- 适用:解释/澄清分支通常不触发工具,没有工具侧
trace_id,但依然希望输出可追踪、可复验(至少能定位到一次解析/路由决策)。 - 代码替换点:只改
has_evidence,其余不动。
def has_evidence(text: str) -> bool:
if not text:
return False
if "R-" in text:
return True
if "RECEIPT:" in text:
return True
if "trace_id=" in text:
return True
if "parse_trace_id=" in text:
return True
return False
不建议把 【解释】、【需要澄清】 当作 evidence 的判断条件:它们是“展示文案”,容易改版;trace_id/parse_trace_id 才是更稳定的“证据字段”。
-
你的解释/澄清输出已经稳定包含
parse_trace_id=,但因为测试只认工具侧trace_id=导致失败(典型现象:R-04 这种澄清用例 fail)。 -
改业务代码(修复系统行为/输出契约不稳定):
-
must_contain里要求的关键字段缺失(例如解释分支缺了no_tools=true或缺了parse_trace_id=),说明输出契约没被稳定执行(典型现象:R-03 fail)。 -
输出内容逻辑不对:该触发工具却没触发、工具入参不合规、工具返回没被回填到最终回复。
-
你希望把“证据字段口径”写进系统实现里,保证任何调用路径(规则/反馈/解释/澄清)输出都一致且可机读。
业务代码改动只选 1 类文件即可(对应根因选点修复):
- 意图识别:
04_sorting_agent_practice/intent.py - 工具契约与返回字段:
04_sorting_agent_practice/tools_sorting.py - 最终输出证据字段口径:
04_sorting_agent_practice/prompts.py - 组装与路由:
04_sorting_agent_practice/app_with_intent.py
1. 意图识别错误:误判/漏判/槽位字段缺失
常见现象:
-
“明明是查规则”却被识别为 GENERAL。
-
“提交反馈”被识别为查规则,导致工具调用错。
-
意图正确但 item_type/task_id/ok/message 抽取缺失或错误。
-
“意图正确但槽位缺失”在工程上通常应视为“需要澄清”,不建议让模型硬猜并继续调用工具。
-
当输入同时包含“规则”和“反馈”字样时,应明确冲突处理优先级;否则会出现批量误判。
实操案例(误判复现与修复:冲突输入):
- 失败用例输入:
提交反馈 t001:按玻璃规则分拣后发现破损 - 期望:SUBMIT_FEEDBACK(含 task_id=t001,ok=False,message 含“破损”)
- 常见误判原因:规则关键词“规则”优先级过高,覆盖了“提交反馈 + task_id”这一更强信号。
- 修复策略示例(规则优先级调整):
- 若检测到 task_id(如 t\d+)或命令式 FEEDBACK,则优先归类为 SUBMIT_FEEDBACK
- 否则再走“规则查询”关键词路径
-
回归:用同一用例 + 3 条相似表达(包含/不包含“规则”)验证不再误判
-
复现:用失败用例原句重跑,保留输入与 trace_id。
-
最小化:只跑意图识别模块,隔离 LLM 与工具干扰(先看规则匹配输出)。
-
定位:看命中规则/关键词/正则路径,确认是哪条规则导致误判。
-
修复:优先修正规则(更确定);必要时增加“澄清提问”分支(need_clarification)。
-
回归:重跑标注数据与关键功能用例,统计是否提升且不引入新错例。
2. 工具调用失败:不触发/入参不合规/工具异常
常见现象:
- 用户说“查规则”,但没有触发工具(路由没选中、系统提示词限制、模型选择不稳定)。
- 工具被触发但报错(入参为空/类型不对/外部依赖未就绪)。
- 工具返回但最终回复没引用证据(没有把工具结果回填到输出模板)。
知识点校验(快速判断法):
- 如果日志/消息链里完全看不到工具调用记录:优先检查意图识别与路由(“工具没触发”)。
- 如果能看到工具调用记录但工具输出是 ERROR:优先检查入参抽取与输入校验(“工具执行失败”)。
- 如果工具输出正常但最终回复无 rule_id/receipt/trace_id:优先检查输出模板约束与“工具结果回填”链路(“证据没被引用”)。
实操案例(工具不触发:路由没选中):
- 输入:
查一下 battery 的分拣规则 - 观察点:
- 意图识别是否输出 intent=QUERY_RULE item_type=battery
- 路由是否把 QUERY_RULE 分发到 query_sorting_rule 工具
- 典型根因:
- item_type 抽取失败(None/空字符串),导致路由判断条件不满足
- 路由只匹配“RULE 命令式输入”,未覆盖中文“查一下…规则”
- 修复后回归:
- 重跑 F-01(glass)与此用例(battery),确保均能触发工具
- 判定故障类型:先确认“工具是否被触发”(看消息链/日志/trace_id)。
- 核对路由:检查意图结果是否正确、工具选择逻辑是否覆盖该输入。
- 核对入参:检查槽位字段是否抽取成功、是否做了空值校验与默认值策略。
- 核对契约:工具输出是否含 rule_id/receipt/trace_id,格式是否被后续解析破坏。
- 回归:重跑工具测试用例 + 功能用例,确保输出稳定。
3. 结果反馈异常:回执缺失/状态不一致/证据链断裂
常见现象:
-
反馈 ok/FAIL 与用户输入语义不一致(抽取规则错误)。
-
输出内容“看似合理”但无依据(未引用回执或规则编号)。
-
优先修复“输出模板强约束”:要求最终答复必须引用 rule_id/receipt/trace_id,否则提示重新执行或追问澄清。
-
对抽取结果加一致性检查:task_id、ok、message 必须齐全,否则进入澄清分支。
实操案例(回执缺失:契约与模板同时检查):
- 输入:
提交反馈 t002 正常,已按规则处理完成 - 期望:输出包含
RECEIPT: task_id=t002 ... | trace_id=...,最终回复引用 receipt 或 trace_id - 若最终回复没有任何 receipt/trace_id:
- 检查工具输出是否真的包含 receipt(工具契约)
- 检查最终回复是否被强约束引用工具输出(Prompt/模板约束)
- 修复后回归:重跑 F-02 + 本用例,确保每次都有可复验字段
目标:让每个小组先拿到一个“稳定可复现”的失败(这比直接讲原理更有效)。
文件:04_sorting_agent_practice/regression_test.py
运行(PowerShell,在项目根目录执行):
cd ".\04_sorting_agent_practice"
python .\regression_test.py
出现 RuntimeError: regression_failed: [...] 是正常现象:表示有用例不满足你写的输出契约(must_contain + evidence)。
- 失败用例编号(例如 R-03/R-04)
- 失败输出片段(
regression_failed列表里会给出截断内容) - 该输出是否包含证据字段:
trace_id=/parse_trace_id=/R-.../RECEIPT:
目标:让 AI 做“分析与建议”,但不允许它“直接替你改完”。
证据三件套(必须给全):
- 失败用例:原句、期望(must_contain)、实际输出片段
- 日志与关键片段:trace_id 或 parse_trace_id、路由选择信息、工具输出(如果有)
- 相关代码片段:只贴与本次失败相关的 10–30 行(intent/路由/输出模板/工具契约)
文件建议:ai_debug_prompt.txt(保存你给 AI 的输入,便于复盘)
你是智能体调试助手。请基于我提供的【失败用例】【日志】【关键代码片段】完成:
1)判断故障类型:意图识别/路由/工具入参/工具执行/结果回填;
2)给出最可能的根因(按优先级排序);
3)给出可落地的修复方案(明确修改点与代码片段);
4)给出复验步骤(至少 3 条),并写清楚预期输出中必须出现的证据字段。
输出必须结构化(分点或 JSON 均可),不要编造不存在的日志或代码。
目标:把 AI 的建议落成可运行的代码改动,并用回归脚本证明“修复有效且不引入新问题”。
允许改动的范围(只选 1 类):
- 意图识别:
04_sorting_agent_practice/intent.py - 工具契约与返回字段:
04_sorting_agent_practice/tools_sorting.py - 最终输出证据字段口径:
04_sorting_agent_practice/prompts.py - 组装与路由:
04_sorting_agent_practice/app_with_intent.py
人工审计检查(写进小组复盘记录,最低要求):
- 你改了哪一个文件、哪一个逻辑点(1–2 句)
- 你为什么确定它不会引入副作用(1–2 句)
- 输出契约是否更稳定(证据字段是否更一致)
修复后回归(必须做):
cd ".\04_sorting_agent_practice"
python .\regression_test.py
- 不再出现
regression_failed - 规则查询/反馈回执/解释/澄清 4 类输出中,至少都能看到稳定的“可复验证据字段”(
trace_id=或parse_trace_id=)
目标:用脚本输出“改前/改后”的混淆矩阵,避免只靠感觉。
文件:04_sorting_agent_practice/data.jsonl
{"text":"查一下 glass 的分拣规则","intent":"QUERY_RULE","item_type":"glass"}
{"text":"查一下 battery 的分拣规则","intent":"QUERY_RULE","item_type":"battery"}
{"text":"glass 怎么分拣?","intent":"QUERY_RULE","item_type":"glass"}
{"text":"battery 如何分拣","intent":"QUERY_RULE","item_type":"battery"}
{"text":"RULE glass","intent":"QUERY_RULE","item_type":"glass"}
{"text":"RULE battery","intent":"QUERY_RULE","item_type":"battery"}
{"text":"怎么分拣?","intent":"QUERY_RULE"}
{"text":"查一下分拣规则","intent":"QUERY_RULE"}
{"text":"提交反馈 t001 FAIL 玻璃破损","intent":"SUBMIT_FEEDBACK","task_id":"t001","ok":false,"message":"玻璃破损"}
{"text":"提交反馈 t002 OK 已按规则处理","intent":"SUBMIT_FEEDBACK","task_id":"t002","ok":true,"message":"已按规则处理"}
{"text":"反馈 t003:按玻璃规则分拣后破损","intent":"SUBMIT_FEEDBACK","task_id":"t003","ok":false,"message":"按玻璃规则分拣后破损"}
{"text":"FEEDBACK t004 FAIL battery 鼓包","intent":"SUBMIT_FEEDBACK","task_id":"t004","ok":false,"message":"battery 鼓包"}
{"text":"怎么处理 t005 这单 battery 鼓包?","intent":"SUBMIT_FEEDBACK","task_id":"t005","ok":false,"message":"battery 鼓包"}
{"text":"提交反馈 t006 失败,查一下 glass 的分拣规则后发现破损","intent":"SUBMIT_FEEDBACK","task_id":"t006","ok":false,"message":"查一下 glass 的分拣规则后发现破损"}
{"text":"为什么要做意图识别?","intent":"GENERAL"}
{"text":"规则和反馈有什么区别?","intent":"GENERAL"}
{"text":"电池为什么要隔离?","intent":"GENERAL"}
{"text":"你好","intent":"GENERAL"}
文件:04_sorting_agent_practice/eval_intent_accuracy.py
运行(PowerShell,在项目根目录执行):
cd ".\04_sorting_agent_practice"
python .\eval_intent_accuracy.py
-
输出 overall_acc + 关键类准确率
-
打印混淆矩阵并指出“最常错的类对”,说明你这次改动对它的影响
-
方式 1:现场演示(推荐)
-
在老师巡回到你组时,现场运行命令并展示输出。
-
在 IDE 中展示你改动的关键代码片段(只看本次改动点)。
- 至少 10 条测试用例(功能 ≥ 4、意图准确率 ≥ 3、工具调用 ≥ 3)。
- 至少 1 个问题完成“复现→修复→回归”闭环,证据链完整(含 trace_id 或 parse_trace_id)。
- AI 协同调试记录 ≥ 1 个问题,且能说明“AI 建议是什么、你审计了什么、你最终改了什么”。
现场出示的证据清单(不收文件,现场查看即可):
- 终端输出:
python .\regression_test.py的运行结果(通过或失败列表)python .\eval_intent_accuracy.py的运行结果(overall_acc + 混淆矩阵)- 输出证据字段:最终回复里至少能看到
trace_id=或parse_trace_id=(按你组选择的口径)。 - 代码改动点:在 IDE 中定位到你改的 1 个文件、1 个逻辑点(例如规则优先级/输出模板/证据字段拼接)。
- AI 交互证据:展示 1 次关键交互(可直接打开聊天记录/截图),并口述你的审计结论。
- 在
regression_test.py新增 1 条回归用例,能覆盖你修复的边界情况。 - 能解释“为什么选择改业务代码/为什么选择调整 has_evidence 口径”,并说明风险与后续收紧标准的计划。