Skip to content

Latest commit

 

History

History
361 lines (216 loc) · 23.3 KB

ch08.md

File metadata and controls

361 lines (216 loc) · 23.3 KB

ch08 即插即用的工具系统:开发记录

阶段 1:brainstorm 定稿(2026-10-09)

用户的关键原话

  • "@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."

Claude 的关键产出

  • 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 的验收标准。

翻车与返工

  • 无。

阶段 2:计划评审通过(2026-10-09)

用户的关键原话

  • spec 审阅:"I approve"。
  • 计划审阅:"yes, continue"。

Claude 的关键产出

  • plan:docs/superpowers/plans/2026-10-09-ch08-tool-system.md(12 个任务,提交 cfcd4b1)。
  • 执行方式:按 CLAUDE.md,Codex 编码,Claude 逐任务检查 diff、跑测试、提交。
  • 版本核对:langchain-mcp-adapters 0.3.2 要求 mcp<2,MCP Server 用 v1 FastMCP(构造函数传 host、port、stateless_http、json_response),不用 v2 MCPServer。
  • 计划阶段对 spec 的补充(已同步 spec):approvals(call id → approved 或拒绝原因)代替 approved_ids;先按发给模型的 Schema 校验,再注入 conversation_id;offer_refund_form 的本轮 Schema 用 enum 收窄订单号,删除 invalid_order 分支;测试默认把审计写进内存列表;MCP runner 用 ToolCall 形式调用取 structured_content。
  • 读代码时发现的风险:不用 db fixture 的测试调用执行引擎时,get_sessionmaker() 会连开发库,审计行会写进开发库。计划用 autouse fixture audit_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 必须兜住。

任务 1:审计表、ORM、审计写入器(2026-10-09)

用户的关键原话

  • 无(按计划执行)。

Claude 的关键产出

  • 提交 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 fixture audit_log 和 db_audit。
  • Codex 报告 RED(ImportError: ToolAuditLog)→ GREEN;Claude 复跑全量 685 passed。
  • 开发库用 mysql --default-character-set=utf8mb4 < db/schema_ch08.sql 建表,HEX(COLUMN_TYPE) 含 E69D83E99990E68B92E7BB9D。

用户拒绝或纠偏的内容

  • 无。

翻车与返工

  • 无。

任务 2:注册表、内置工具包、本轮工具集(2026-10-09)

用户的关键原话

  • 无(按计划执行)。

Claude 的关键产出

  • 提交 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 的导入路径(全仓库迁移需要)。

任务 3:执行引擎(2026-10-09)

用户的关键原话

  • 无(按计划执行)。

Claude 的关键产出

  • 提交 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("工具未开放")。

用户拒绝或纠偏的内容

  • 无。

翻车与返工

  • 无。

任务 4:策略文件(2026-10-09)

用户的关键原话

  • 无(按计划执行)。

Claude 的关键产出

  • 提交 ddd07a8:app/tools/policy.py(按 (path, mtime_ns) 缓存;文件读失败或结构不对时沿用上次内容,首次失败为空策略,打 tool_policy_invalid;未知权限值按 deny)、config/tools.json(logistics 8101、aftersales 8102)、apply_override。全量 751 passed。

用户拒绝或纠偏的内容

  • 无。

翻车与返工

  • 无。

任务 5:两个 MCP Server(2026-10-09)

用户的关键原话

  • 无(按计划执行)。

Claude 的关键产出

  • 提交 f6e43ae:mcp_servers/(common、logistics 8101、aftersales 8102,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 停进程时要按进程名或进程组停。

任务 6:MCP 发现、合并工具集、物流改由 MCP 提供(2026-10-09)

用户的关键原话

  • System 预留超出时的处理:"Raise reserve to 2400 (Recommended)"。

Claude 的关键产出

  • 提交 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_ticket 169 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 的重试保留。

任务 7:ticket_request 分流(2026-10-09)

用户的关键原话

  • 无(按计划执行)。

Claude 的关键产出

  • 提交 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.py 30/30(含 6 轮 ticket_request 检查);run_intent_eval.py 退出码 0,≥0.9 档准确率 97.7%。

用户拒绝或纠偏的内容

  • 无。

翻车与返工

  • 无。

任务 8:confirm_write、ticket_reply、Agent 工单规则(2026-10-09)

用户的关键原话

  • 无(按计划执行)。

Claude 的关键产出

  • 提交 ac2e4c1:app/graph/nodes/confirm.py(confirm_write 在 interrupt() 前只做参数校验;ticket_reply 4 种固定话术);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。

任务 9:SSE 预览事件、/chat/resume 确认、作废审计(2026-10-09)

用户的关键原话

  • "If codex runs out of its quota, please use Sonnet subagent to substitue for Codex. Have this rule been written into CLAUDE.md?"

Claude 的关键产出

  • 提交 6d0a944:stream_graph 按 interrupt 类型发 ticket_preview 或 order_picker;ResumeRequest 增加 ticket_confirm,order_id 和 ticket_confirm 必须恰好一个(否则 422);预检按类型匹配,没有待确认工单时 409 no_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"改为长期规则。

翻车与返工

  • 无。

任务 10:工单 Prompt 评估集(2026-10-09)

用户的关键原话

  • "this chapter use sonnet for remaining coding tasks. Only in this chapter, donot modify CLAUDE.md"

Claude 的关键产出

  • 提交 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。

任务 11:前端工单预览卡片(Vibe Coding,2026-10-09)

用户的关键原话

  • 原始需求:"前端渲染成预览卡片,带「确认提交」「取消」两个按钮"。
  • 执行者:按用户要求由 Sonnet 子代理编码(本章剩余任务)。

Claude 的关键产出

  • 提交 9c0038d:renderTicketPreview(标题"请确认工单信息"、工单类型、问题描述,全部用 textContent);「确认提交」「取消」点击后两个都置灰,调 /chat/resume(ticket_confirm);409 时卡片下显示"该工单已失效,请重新发起";updateControls 也管理工单按钮;按钮样式与发送按钮一致(主按钮蓝底,取消为描边次按钮)。
  • Chrome 验收(录屏 ch08_ticket_preview_confirm_cancel.gif 已下载):
    1. "帮我建个工单" → Agent 追问问题描述;补"蓝牙耳机左耳没声音,买了一周" → 卡片出现。
    2. 新会话一句话说清 → 卡片(售后)→ 点确认 → 回复"已为您创建工单 T20261009001,类型:售后";tickets 有该行,审计 create_ticket 成功 retry_count=0 duration_ms=35。
    3. 新会话"给我提个投诉工单,快递把箱子摔坏了" → 卡片(投诉)→ 点取消 → 取消话术;tickets 无新行,审计 权限拒绝 / 用户取消。
    4. "帮我建个咨询工单,想问发票怎么开" → 卡片(咨询),样式检查通过。

用户拒绝或纠偏的内容

  • 无。

翻车与返工

  • 第一次浏览器验收:卡片出现但两个按钮一直是灰的。原因:按钮在流式输出期间创建,disabled = streaming 为真;updateControls() 结束时只恢复 .order-card,没管工单按钮。子代理补上后通过。
  • 按钮为浏览器默认样式,与页面不一致,补了样式。
  • 浏览器自动化里按 Return 没发出消息(输入法组合态吞掉了回车),改为点"发送"按钮。属于测试操作问题,不是页面缺陷。

任务 12:验收脚本、演示素材、CLAUDE.md、回归(2026-10-09)

用户的关键原话

  • status_query 的处理方式:"Resolver flag status_query (Recommended)"。

Claude 的关键产出

  • 提交 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.py 16/16;demo6.sh 4/4(多轮 34/34、意图 60/60);demo7.sh 4/4(新预算 5150/3604/1545,级联完整);run_ticket_eval.py 13/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。

code review 结论(2026-10-09)

用户的关键原话

  • 无。

Claude 的关键产出

  • 方式:作者自审(会话规则不允许在用户未要求时启动审查子代理;用户授权的 Sonnet 子代理只做编码),按 code-reviewer.md 清单检查整条分支 e7e0078..82b9c2d(30 个提交),逐项核对 Review Focus 5 条(都有测试)。比独立审查弱;需要独立审查时可运行 /code-review。
  • Critical、Important:无。
  • Minor 5 条,不修:
    1. MCP 工具描述由 Server 编写,原样进模型的工具列表。权限有闸门,但描述文本是提示注入面。
    2. 工单预览待确认时点投诉流程的建工单按钮或退款按钮,不写"已被新消息取代"的审计,也会清掉待确认状态(与 backlog 2026-10-08-ticket-during-pending-picker 同类)。
    3. 读超时的回复把"转人工入口"说了两遍(模型措辞)。
    4. 多轮评估 ticket-1-2 3 次中 1 次失败(模型波动)。
    5. 每轮 Agent 和每次恢复都重新发现 MCP 工具,Server 连不上时最多多等 2 秒。

用户拒绝或纠偏的内容

  • 无。

翻车与返工

  • 无。

finish(2026-10-09)

用户的关键原话

  • "1. AND PUSH TO REMOTE REPO"(选择本地合并回 main,并推送远程)。
  • 工作要求:"完结交付:功能演示命令、测试结果、dev-notes 路径"。

Claude 的关键产出

  • 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.sh 6/6;run_ticket_eval.py 13/13;run_multiturn_eval.py 34/34;run_intent_eval.py 60/60;run_tool_selection_eval.py 16/16;demo6.sh 4/4;demo7.sh 4/4;Chrome 验收确认、取消各一次。
  • 开发记录:dev-notes/ch08.md。

用户拒绝或纠偏的内容

  • 无。

翻车与返工

  • 无(本章的翻车与返工记在各任务段)。