- "@ch08.sql 这是要建的表"。
- "我要用 Superpowers 模式把客服系统的工具层从几个写死的内置工具,升级成即插即用的工具系统。"
- 功能需求 7 项(注册中心、参数校验、权限控制、执行引擎、审计留痕、MCP 接入、建工单确认流);验收 6 条;本章不做 Skill 机制、更多外部系统。
- 技术栈:"MCP Server:用 MCP 官方 Python SDK 自建,走 Streamable HTTP 传输";"MCP Client:用 LangChain 官方 langchain-mcp-adapters 的 MultiServerMCPClient 多 Server 接入"。
- 明确建工单的分流:"A"(解析器加
ticket_request标记,直达 Agent)。 - 未列出的 MCP 工具的权限:"A"(默认拒绝,策略文件加一行才放行)。
- 设计第 1 至 3 段:"ok"。
- 分支:"why not created from main branch? i think main branch has already merged ch06, or am i wrong?";"ok, merge and push first. Before merging, check if this operation will affect the criterion for ch07 goals and modification."
-
spec:
docs/superpowers/specs/2026-10-09-ch08-tool-system-design.md。 -
分类:架构级(新子系统、Agent 绑定方式和图结构变化、新表、新进程)。
-
方案:方案 1,我方执行引擎是唯一入口,MCP 工具包装成注册表条目;不用
tool_interceptors双路径,不改用create_agent中间件。 -
合并 PR #2:合并前核对
origin/main是ch06的祖先、合并结果的 tree 与ch06相同(f685190),全量测试 680 passed(与 ch07 finish 相同)。gh pr merge 2 --merge后main为e7e0078,ch08从main拉出。 -
用户确认的决策:
问题 决策 明确建工单的分流 resolve_reference输出ticket_request,为真时after_intent直接走 business,优先于投诉和history_recall;8 类意图不变MCP 工具权限 config/tools.json,mtime 变化即重读;未列出、deny、write一律拒绝;不看 Server 的 annotations内置工具 app/tools/builtin/包扫描,导入即登记;新增需重启服务,不改核心代码执行引擎 查找 → 权限 → JSON Schema 校验( jsonschema)→ 超时/重试(只读工具、暂时性故障)→ 分诊 → 格式化 → 审计查询落空的审计状态 成功,error_message="查询落空"确认流 新节点 confirm_write在interrupt()前只做纯计算;approved_ids只来自确认节点和POST /tickets;确认或取消后ticket_reply用固定话术收尾,不再调模型query_logistics生产清单下线,移到 app/tools/legacy/,只给 ch04 评估基线故障注入 Server 侧 MOCK_DELAY_SECONDS+ 策略文件overrides;不加新配置项
- Claude 提议从
ch06拉分支(PR #2 未合并)。用户以为main已合并 ch06;Claude 核对后说明未合并,用户要求先合并 PR #2 再从main拉分支,并要求先确认合并不影响 ch07 的验收标准。
- 无。
- spec 审阅:"I approve"。
- 计划审阅:"yes, continue"。
- plan:
docs/superpowers/plans/2026-10-09-ch08-tool-system.md(12 个任务,提交cfcd4b1)。 - 执行方式:按 CLAUDE.md,Codex 编码,Claude 逐任务检查 diff、跑测试、提交。
- 版本核对:
langchain-mcp-adapters0.3.2 要求mcp<2,MCP Server 用 v1FastMCP(构造函数传host、port、stateless_http、json_response),不用 v2MCPServer。 - 计划阶段对 spec 的补充(已同步 spec):
approvals(call id →approved或拒绝原因)代替approved_ids;先按发给模型的 Schema 校验,再注入conversation_id;offer_refund_form的本轮 Schema 用enum收窄订单号,删除invalid_order分支;测试默认把审计写进内存列表;MCP runner 用 ToolCall 形式调用取structured_content。 - 读代码时发现的风险:不用
dbfixture 的测试调用执行引擎时,get_sessionmaker()会连开发库,审计行会写进开发库。计划用 autouse fixtureaudit_log防止。 - Review Focus 5 条:测试连开发库、待确认时 MCP Server 停了、模型自带
conversation_id、重复确认或新消息后点旧卡片、MCP 只返回文本。每条都指定了测试。 - 回归影响:ch05 至 ch07 的演示脚本问物流时需要 logistics MCP Server,任务 12 加端口检查。
- 无。
- 计划自查发现 4 处问题并当场改:任务 1 的中文校验改用
HEX();任务 3 测试辅助函数的raises逻辑混乱;任务 8 写超时的注入方式不具体;discover()整体抛异常时build_base_toolset必须兜住。
- 无(按计划执行)。
- 提交
d7eaf90:db/schema_ch08.sql(Claude 用git mv移动ch08.sql,校验 SHA1 不变)、compose 挂载06-schema-ch08.sql、reset_db.sh表检查和"权限拒绝"枚举的HEX()校验、ToolAuditLog、app/repositories/audit.py、app/tools/audit.py(1 秒超时、吞异常、audit_write_failed)、autouse fixtureaudit_log和db_audit。 - Codex 报告 RED(
ImportError: ToolAuditLog)→ GREEN;Claude 复跑全量 685 passed。 - 开发库用
mysql --default-character-set=utf8mb4 < db/schema_ch08.sql建表,HEX(COLUMN_TYPE)含E69D83E99990E68B92E7BB9D。
- 无。
- 无。
- 无(按计划执行)。
- 提交
61066fa:ToolEntry、register()、builtin_registry()(pkgutil扫描app/tools/builtin/)、Toolset、builtin_toolset()、ch04_tools()/ch04_toolset();内置工具移入builtin/;Agent 改为绑定Toolset.agent_tools()(字典形式);ch04 两个评估脚本改用ch04_tools()。 - 重构任务,行为不变:绑定的 4 个工具和顺序、退款单条件、
invalid_order分支都保留。全量 690 passed。 - 裁定:
pkgutil按模块名字母序扫描,turn_toolset暂按原AGENT_TOOLS顺序重排;任务 6 须固定绑定顺序(内置固定顺序在前,MCP 按策略文件顺序在后),保证前缀缓存稳定。
- 无。
- 无。Codex 额外改了
tests/test_retrieval.py中query_faq的导入路径(全仓库迁移需要)。
- 无(按计划执行)。
- 提交
bc655ac:app/tools/validation.py(jsonschema Draft 2020-12 → 中文短句);执行引擎按"查找 → 权限 → 校验 → 注入 → 执行(只读工具对暂时性故障重试)→ 分诊 → 格式化 → 审计 +tool_call日志"重写;retry_async加should_retry、on_retry;POST /tickets传approvals。 - 核对:
on_retry只在真正重试前调用,3 次都失败时retry_count=2;写工具无论max_retries多少都不重试;审计arguments为注入前的原参数。全量 727 passed。 - 现有测试的断言变化:Agent 调未开放的工具,错误码从
unknown_tool变为permission_denied("工具未开放")。
- 无。
- 无。
- 无(按计划执行)。
- 提交
ddd07a8:app/tools/policy.py(按(path, mtime_ns)缓存;文件读失败或结构不对时沿用上次内容,首次失败为空策略,打tool_policy_invalid;未知权限值按deny)、config/tools.json(logistics 8101、aftersales 8102)、apply_override。全量 751 passed。
- 无。
- 无。
- 无(按计划执行)。
- 提交
f6e43ae:mcp_servers/(common、logistics8101、aftersales8102,aftersales 启动时导入tools/下全部模块并调用register(mcp));依赖mcp>=1.24,<2、langchain-mcp-adapters>=0.3.2。全量 773 passed。 - Claude 起进程实测:
POST /mcp tools/list返回query_logistics,inputSchema带订单号pattern。
- 无。
- Codex 发现 FastMCP 1.x 对返回注解
-> dict不生成structuredContent,改为-> dict[str, Any],并补回归测试(先 2 failed 后通过)。 - Claude 手动验证时
kill %1只停了uv外层进程,Server 子进程仍在;改用pkill -f mcp_servers.logistics。demo8.sh 停进程时要按进程名或进程组停。
- System 预留超出时的处理:"Raise reserve to 2400 (Recommended)"。
- 提交
a447e00:app/tools/mcp.py(每个 Server 单独get_tools,2 秒超时并行;发现失败的 Server 的read工具进unavailable;内置优先,重名跳过;toolset日志)、app/tools/formatters.py(3 个 MCP 工具的字段挑选和编码翻译)、turn_toolset(agent=False关闭;退款单order_id用enum收窄;ticket_request为假时关闭create_ticket)、query_logistics移到app/tools/legacy/;绑定顺序固定(内置BUILTIN_AGENT_ORDER在前,MCP 按策略文件顺序在后);invalid_order分支删除。 - 提交
a447e00同时把SYSTEM_RESERVE_TOKENS从 1900 改为 2400(用户裁定),演示配置预算 5150/3604/1545;同步tests/test_budget.py、scripts/demo7.sh、ch07 spec 附注、ch08 spec 4.6、计划 Global Constraints、CLAUDE.md。 - 修复提交
299b75d:本机 MCP Server 用trust_env=False的 httpx 客户端直连。全量 806 passed。
- 无。用户在 3 个方案中选了推荐方案(提高预留)。
- 全量剩 1 条失败:
test_current_system_prompt_and_tools_fit_reserve实测 1922 > 1900(create_ticket169 token 替换了query_logistics)。Claude 起两个 Server 实测每轮 System + 工具定义 2047–2360 token,每一轮都超预留,不只是启动自检。这与 ch07 用户认可的演示预算 5650 冲突,停下来问用户。 - Codex 实测 Server 停掉后调用抛
ExceptionGroup → HTTPStatusError(502)。Claude 追查:本机 shell 设了HTTP_PROXY、HTTPS_PROXY、ALL_PROXY,没有NO_PROXY,httpx 把127.0.0.1的请求也发给了代理;Task 6 的测试能通过,是因为代理替我们转发了。代理关掉时本机 MCP 会全部失败。修复:本机地址的 Server 用trust_env=False的客户端;先写"代理指向不可连端口"的测试,RED(发现失败)后 GREEN。is_transient对 502/503/504 的重试保留。
- 无(按计划执行)。
- 提交
3ab46bc:ResolvedQuery/Resolution加ticket_request(历史为空也取,解析失败为 False);指代消解 Prompt 加## ticket_request一节;after_intent第一条规则为ticket_request→ business;State 字段由start_turn重置;多轮评估集加ticket-1、ticket-2两组。全量 821 passed。 - 真实上游:
run_multiturn_eval.py30/30(含 6 轮ticket_request检查);run_intent_eval.py退出码 0,≥0.9 档准确率 97.7%。
- 无。
- 无。
- 无(按计划执行)。
- 提交
ac2e4c1:app/graph/nodes/confirm.py(confirm_write在interrupt()前只做参数校验;ticket_reply4 种固定话术);after_agent在ticket_request且有create_ticket调用时进confirm_write;after_tools有写决定时进ticket_reply;agent_tools写write_outcome;Agent System Prompt 加## 建工单一节,改"工具结果"第 4 条和"行为约束"第 4 条;create_ticket工具描述改为"只在用户明确要求建工单时调用。调用后系统会请用户确认,确认后才创建"。全量 838 passed。 - 测试覆盖:预览前无事件和写库、确认建单、取消(审计"用户取消")、参数不合法不中断、两个调用只建第一个、写超时不重试、未开放时拒绝、待确认时 MCP 发现失败仍能建单、
turn日志字段、失败话术、下一轮重置。
- 无。
- 计划与 spec 冲突:执行引擎先查确认、后校验参数,参数不合法的
create_ticket得到"权限拒绝/未经用户确认",模型无从知道缺什么。spec 5.3 要求返回"校验拦下"。Codex 按"只做 brief 范围"停下来问。Claude 裁定:写工具先校验、后查确认(校验是纯计算,未确认时仍不执行);补测试test_write_tool_invalid_args_are_blocked_before_approval,RED 后 GREEN。
- "If codex runs out of its quota, please use Sonnet subagent to substitue for Codex. Have this rule been written into CLAUDE.md?"
- 提交
6d0a944:stream_graph按 interrupt 类型发ticket_preview或order_picker;ResumeRequest增加ticket_confirm,order_id和ticket_confirm必须恰好一个(否则 422);预检按类型匹配,没有待确认工单时 409no_pending_confirmation;prepare_chat_turn发现待确认的工单时写一条审计"权限拒绝/用户未确认,已被新消息取代"。全量 850 passed。 - 测试覆盖 Review Focus 4:确认后再确认一次 → 409,工单只有一条;新消息后点旧卡片 → 409,工单无行。
- 回答用户:该规则原来不在 CLAUDE.md,只在 Claude 的自动记忆中,且只限 ch07(其他章节默认暂停)。按用户要求写进 CLAUDE.md 的 Codex 一节(提交
d46a200):每次codex exec后查额度错误;额度用完时告诉用户并改派 Sonnet 子代理执行同一任务描述,Claude 照常审查。删除旧的记忆文件,避免两处规则不一致。
- 用户纠正了额度规则的适用范围:从"只限 ch07"改为长期规则。
- 无。
- "this chapter use sonnet for remaining coding tasks. Only in this chapter, donot modify CLAUDE.md"
- 提交
0300a4c:evals/ticket_samples.jsonl(12 条:4 条追问、8 条建单)、evals/run_ticket_eval.py(按生产拼装做第 1 次调用,并发 3,全部通过才退出码 0)、tests/test_ticket_samples.py(判分函数单测);Agent Prompt## 建工单新增第 3 条"用户讲清问题后,直接调用 create_ticket,不先查询订单或物流。用户给出的订单号原样写进 description"。全量 884 passed。 - 真实上游:第 1 轮 11/12,第 2 轮 11/12,第 3 轮 12/12。
- 用户新要求:本章剩余的编码任务改由 Sonnet 子代理完成,只限本章,不改 CLAUDE.md。Claude 记入自动记忆(只限 ch08),CLAUDE.md 不动。
- 用户把本章剩余编码任务的执行者从 Codex 改为 Sonnet 子代理。
- 第 1 轮 t07 失败:用户给了订单号,模型先调
query_order,没调create_ticket。裁定改 Prompt,不放宽判分:mock 对未知订单号返回随机数据,先查订单会把无关信息带进工单。 - 第 2 轮 t02 失败,是判分误伤:描述"用户反馈蓝牙耳机左耳没有声音,购买约一周,要求建工单处理。"含事实,但含子串"要求建工单"。判分改为先去掉套话再看剩余字数(≥ 4),补 4 条单测,RED 后 GREEN。
- 原始需求:"前端渲染成预览卡片,带「确认提交」「取消」两个按钮"。
- 执行者:按用户要求由 Sonnet 子代理编码(本章剩余任务)。
- 提交
9c0038d:renderTicketPreview(标题"请确认工单信息"、工单类型、问题描述,全部用textContent);「确认提交」「取消」点击后两个都置灰,调/chat/resume(ticket_confirm);409 时卡片下显示"该工单已失效,请重新发起";updateControls也管理工单按钮;按钮样式与发送按钮一致(主按钮蓝底,取消为描边次按钮)。 - Chrome 验收(录屏
ch08_ticket_preview_confirm_cancel.gif已下载):- "帮我建个工单" → Agent 追问问题描述;补"蓝牙耳机左耳没声音,买了一周" → 卡片出现。
- 新会话一句话说清 → 卡片(售后)→ 点确认 → 回复"已为您创建工单 T20261009001,类型:售后";
tickets有该行,审计create_ticket 成功 retry_count=0 duration_ms=35。 - 新会话"给我提个投诉工单,快递把箱子摔坏了" → 卡片(投诉)→ 点取消 → 取消话术;
tickets无新行,审计权限拒绝 / 用户取消。 - "帮我建个咨询工单,想问发票怎么开" → 卡片(咨询),样式检查通过。
- 无。
- 第一次浏览器验收:卡片出现但两个按钮一直是灰的。原因:按钮在流式输出期间创建,
disabled = streaming为真;updateControls()结束时只恢复.order-card,没管工单按钮。子代理补上后通过。 - 按钮为浏览器默认样式,与页面不一致,补了样式。
- 浏览器自动化里按 Return 没发出消息(输入法组合态吞掉了回车),改为点"发送"按钮。属于测试操作问题,不是页面缺陷。
status_query的处理方式:"Resolver flag status_query (Recommended)"。
- 提交
82b9c2d:scripts/demo8.sh(自启停两个 MCP Server 和服务,6 项验收,EXIT 时还原config/tools.json、删除复制的素材)、scripts/demo8_chat.py(trust_env=False)、两个演示素材、tests/test_demo8_files.py;demo5/6/7 开头检查 8101 端口;CLAUDE.md 加 ch08 状态、命令、架构、模块表和约束(Claude 改)。编码由 Sonnet 子代理完成。 - 提交
2e36ba7:解析器新增status_query(用户裁定),问自己订单的实时状态时直达 Agent;多轮评估集加status-1组。 - 提交(
fix(ch08): ticket prompt asks only for the problem…):## 建工单第 2 条改为"工单只需要两项……不追问订单号、购买时间等其他信息";工单评估集加 t13。 bash scripts/demo8.sh /tmp/ch08-demo:6/6 通过。审计实测:读超时query_logistics mcp logistics 超时 retry_count=2 duration_ms=6432;写超时create_ticket 超时 retry_count=0;取消权限拒绝/用户取消;确认建单 T20261009002。- 回归(两个 MCP Server 运行中):
run_tool_selection_eval.py16/16;demo6.sh4/4(多轮 34/34、意图 60/60);demo7.sh4/4(新预算 5150/3604/1545,级联完整);run_ticket_eval.py13/13;全量 903 passed。
- 无。
- demo8 第 1 次:第 3 项失败。"帮我查一下订单 1001 的维修进度"识别为售后,走 aftersales 出口,检索 Top-1 0.1995 低于闸门 0.20,回兜底话术,Agent 没机会调新 MCP 工具。这暴露了一个设计缺口:售后 MCP 工具(退货进度、维修进度)对应的问题大多过不了检索闸门。停下来问用户,用户选新增
status_query。 status_query多轮评估:第 1 次 32/34,status-1第 2、4 轮"指代错"是 Claude 在 brief 里把需要补全指代的轮标成了unchanged,改为must_contain_any;第 2 次status-1-4改写成"退掉",把关键词放宽为"退";ticket-1-2偶发把"帮我建个工单"拼进完整句子(违反指代消解规则 2),之后连跑 3 次都通过,记为 3 次中 1 次的模型波动。- demo8 第 2 次:第 4 项失败。Agent 先问"订单号是多少",用户只答了问题描述,Agent 又追问订单号,没有建单。订单号不是
create_ticket的必填项。改 Prompt 第 2 条并加评估样例 t13(13/13),demo8 第 3 次 6/6。
- 无。
- 方式:作者自审(会话规则不允许在用户未要求时启动审查子代理;用户授权的 Sonnet 子代理只做编码),按
code-reviewer.md清单检查整条分支e7e0078..82b9c2d(30 个提交),逐项核对 Review Focus 5 条(都有测试)。比独立审查弱;需要独立审查时可运行/code-review。 - Critical、Important:无。
- Minor 5 条,不修:
- MCP 工具描述由 Server 编写,原样进模型的工具列表。权限有闸门,但描述文本是提示注入面。
- 工单预览待确认时点投诉流程的建工单按钮或退款按钮,不写"已被新消息取代"的审计,也会清掉待确认状态(与 backlog
2026-10-08-ticket-during-pending-picker同类)。 - 读超时的回复把"转人工入口"说了两遍(模型措辞)。
- 多轮评估
ticket-1-23 次中 1 次失败(模型波动)。 - 每轮 Agent 和每次恢复都重新发现 MCP 工具,Server 连不上时最多多等 2 秒。
- 无。
- 无。
- "1. AND PUSH TO REMOTE REPO"(选择本地合并回
main,并推送远程)。 - 工作要求:"完结交付:功能演示命令、测试结果、dev-notes 路径"。
ch08快进合并到main(main是ch08的祖先),合并后全量 903 passed;推送origin/main。删除本地ch08分支,远程origin/ch08保留。- 演示命令:
bash scripts/demo8.sh /tmp/ch08-demo(自启停两个 MCP Server 和服务);手工演示先启动uv run python -m mcp_servers.logistics、uv run python -m mcp_servers.aftersales、uv run uvicorn app.main:app --port 8000,打开 http://127.0.0.1:8000/ 说"帮我建个工单"。 - 测试结果:pytest 903 passed;
demo8.sh6/6;run_ticket_eval.py13/13;run_multiturn_eval.py34/34;run_intent_eval.py60/60;run_tool_selection_eval.py16/16;demo6.sh4/4;demo7.sh4/4;Chrome 验收确认、取消各一次。 - 开发记录:
dev-notes/ch08.md。
- 无。
- 无(本章的翻车与返工记在各任务段)。