工具调用循环:从会聊天到能查工单
先看模型闯祸
给模型一句“查一下我的工单”,它可能返回:
或者把工具结果直接写进回答:
后者没有任何工具调用,完全可能是编造。前者的字段名也不是 OpenAI-compatible API 约定的 tool_calls。因此工具调用不是“把几个函数描述塞进 prompt”,而是一套严格的协议循环。
工具契约先写成 Schema
Schema 有三个工程价值:模型知道参数形状;服务端可以拒绝多余字段;评测可以判断「工具选对且参数对」。不要在工具描述里写「如果用户是管理员就返回所有工单」,权限是代码的责任。
search_kb 现在返回空列表——语料要到第 9、10 篇才有。先把契约固定下来是有意的:第 3 篇定义的这个函数签名,到第 11 篇接上混合召回、第 12 篇接上引用时都不用改,变的只有实现。这和第 1 篇的 Repository 是同一个手法。
它也带来本篇第二个坏结果演示:知识库是空的,但模型照样会编。给它「点图标没反应怎么办」,它会跳过工具直接答一段听起来很像样的排查步骤。工具返回空,不等于模型会承认自己不知道——服务端怎么把「有没有答案」变成可计算的量,是第 12 篇的主题。
注册表和最小循环
关键点是工具结果由服务端追加到 messages,模型不能自己 伪造 tool 角色消息。每一步都要有上限;max_steps=6 是防止模型在两个工具之间来回跳转的最后保险。
非法 JSON 不能让请求 500
先写一个会返回坏参数的 Mock:
多出来的那个逗号足以让 json.loads 抛异常。真实模型输出这种东西的频率比想象中高得多——尤其在参数里含中文、换行或引号时。处理方式不是加固解析器,而是给它一条正常的失败分支:
期望不是 500,而是把结构化错误反馈给模型,让它有机会修正:
最多允许一次参数修正;第二次仍失败就终止并记录审计。不要把 Python traceback 放进模型上下文,它既泄露实现细节,也无法帮助模型选择下一步。
当前用户先用桩,但不能信任模型
本篇为了聚焦循环,user 使用固定的 demo-user。工具仍然接收这个参数,并在 Repository 查询时过滤 owner:
第 6 篇会把桩替换为 JWT 和 RBAC,但工具接口不变,这就是先固定边界再替换实现的好处。
测试与排障
至少覆盖:无工具调用、一次调用、多个调用、未知工具、非法 JSON、schema 多余字段、权限拒绝、超过最大步数。调试时打印 tool_name、call_id、耗时和错误码,默认不要打印完整用户问题或工具参数中的密钥。
本篇验收标准
- 「查工单」会产生真实的
tool消息,回答中包含数据库返回的标题。 - 模型伪造的
tool结果不会被服务端接受。 - 非法 JSON 最多触发一次修正,最终返回可读的降级答案。
- 任意输入都不会执行未注册函数;循环最多 6 步。
search_kb返回空列表时,模型仍可能编造答案——把这个行为记录下来,它是第 12 篇拒答阈值的起点。
下一篇先停下来测量:我们要建立一套评测基线,否则后面每次改 prompt 都只能凭感觉。
