Harmony built-in tools、tool session 与二段生成回路#
这章解决什么问题#
前面的结构化生成章节已经讲了普通 function calling、tool parser、Responses API 的 background 语义,以及 MCP tool server,但还有一层很容易让读者误以为“只是内建 demo 工具”的机制没有单独讲:当 Responses API 进入 harmony 路径,并启用内建 browser / python 工具时,系统不再是“一次生成完就结束”,而是会进入一条生成、调用工具、回填工具结果、再继续生成的二段甚至多段回路。
这章的目标,就是把这条 built-in tool 回路讲透。
为什么这层值得单独讲#
如果只把内建工具写成“支持浏览器和 Python”,整本书会漏掉一层很重要的工程事实:这些工具不是在 HTTP handler 外面随手包一层,而是通过 ConversationContext、tool session 和 harmony message 结构真正进入生成回路。
换句话说,它们不是“外插插件”,而是 output loop 的一部分。
一张图:built-in tool 回路为什么不是单段生成#
这张图解决的理解障碍是:很多读者会把 tool 调用想成“模型输出一次 tool call,服务端同步执行后收工”,但 harmony 路径实际上是一个带上下文回填的循环。
flowchart LR
Prompt["engine prompt"] --> Gen1["generate_request(...)"]
Gen1 --> Ctx["HarmonyContext.append_output()"]
Ctx --> Need["need_builtin_tool_call()?"]
Need -->|yes| Tool["call_tool() / tool session"]
Tool --> Msg["tool result message"]
Msg --> Render["render_for_completion()"]
Render --> Gen2["second generation pass"]
Need -->|no| Final["final response"]图比纯文字多解释的一点是:tool 调用不是结果后处理,而是下一轮生成的输入准备阶段。
ConversationContext 才是这条回路的真正骨架#
python/sglang/srt/entrypoints/context.py 定义了一个很清楚的抽象:
append_output(...)need_builtin_tool_call()call_tool()render_for_completion()
这四个动作刚好对应了 built-in tool 回路的四个阶段:
- 吃下模型刚刚生成的输出
- 判断是否需要进入 tool 分支
- 真正调用工具
- 把新消息重新渲染成下一轮 completion 的输入
从图书写法看,这是一个特别适合单独成章的“关键抽象”。因为它把一个看起来分散在 handler、tool server 和 message parser 里的流程压成了稳定接口。
HarmonyContext 和 SimpleContext 的边界非常重要#
SimpleContext 基本不支持 built-in tool 回路;HarmonyContext 才真正实现了:
- message 累积
- tool session 保存
- parser 驱动的 output append
- built-in tool call 判断
这意味着 built-in tool 回路不是所有 API surface 的通用行为,而是 harmony 路径下的一种特定运行人格。把这层边界讲清楚,能避免读者把 Responses API 的全部行为都误当成普通 chat/completions 的自然延伸。
need_builtin_tool_call() 实际在检查什么#
在 HarmonyContext 里,判断条件并不神秘:看最后一条 message 的 recipient 是否以 browser. 或 python 开头。
这说明 built-in tool 回路的入口不是另一个隐藏状态机,而是建立在 message 语义之上:
- 模型/assistant 当前把消息发给谁
- 这个 recipient 是否属于内建工具命名空间
从工程上说,这是一种把“工具分支”编码进消息协议的方式,而不是在 parser 外面再维护一套 parallel state machine。
tool session 为什么要在 AsyncExitStack 里统一管理#
serving_responses.py::create_responses() 会在需要时:
- 为
browser/python建立tool_session_ctxs - 通过
AsyncExitStack统一进入和退出 session
这说明 tool session 不是一次性函数调用,而是有资源生命周期的上下文对象。对 MCP server 尤其如此,因为 session 里可能包含连接、能力协商和工具清单。
这也是为什么 built-in tool 工作流不能简单被理解成“拿到 tool call 就本地调个函数”。
harmony 系统消息为什么会注入工具描述#
在 _construct_input_messages_with_harmony(...) 里,系统会根据 request tools 和 tool server 能力决定是否把:
browser_descriptionpython_description
注入 get_system_message(...)。这说明内建工具不是静默存在的;系统会把它们的描述显式并入 prompt / harmony message 语义。
这件事的工程意义非常直接:
- 模型需要知道当前有哪些内建工具
- tool 能力描述会影响它是否发出
browser.*或pythonrecipient
_generate_with_builtin_tools(...) 为什么让 Responses 变成多段生成#
create_responses() 不直接调用普通 generator,而是走 _generate_with_builtin_tools(...)。这说明 built-in tool 路径从一开始就不把“单轮生成”当作默认假设。
更稳的理解是:
- 第一轮生成负责把 assistant 消息和 tool 意图产出来
- context 决定是否需要工具调用
- 如果需要,先执行工具,再把工具结果消息重新送回上下文
- 再继续下一轮生成,直到不再需要内建工具或达到结束条件
这本质上是一个 runtime orchestrated loop。
browser 和 python 为什么被当成特殊命名空间#
HarmonyContext.call_tool() 里有明确分支:
recipient.startswith("browser.")recipient.startswith("python")
前者会进一步拆出动作名,例如 browser.search、browser.open、browser.find;后者则把最后一条消息内容当成代码交给 Python tool。
这说明两个 built-in tool 家族虽然都叫“工具”,但协议形状并不一样:
- browser 更像多 action namespace
- python 更像单 action、代码体即参数的执行器
这种差异值得技术书主动讲出来,因为它解释了为什么不同 built-in tool 会对消息格式提出不同要求。
为什么 background / streaming 与 MCP tool server 有额外限制#
serving_responses.py 明确拒绝某些组合:
MCPToolServer- 同时
background或stream - 且使用
web_search_preview/code_interpreter之类工具
这是一个非常值得写进书里的行为边界。它表明工具工作流并不是所有表面组合都完全兼容。原因从代码上看不是“协议上不喜欢”,而是这类组合会让 session 生命周期、流式输出和后台存储语义同时变复杂。
这里仍然区分 事实 与 推断:
事实:源码明确返回错误,禁止这些组合推断:上游这么限制,是为了避免 tool session 与 background/streaming 生命周期交织失控
这一层最容易踩的坑#
1. 以为 built-in tool 只是 tool server 的一个 UI 别名#
它实际进入了 ConversationContext 和多轮生成回路。
2. 以为工具结果只是普通日志或 side effect#
工具结果会被重新包装成 message,再进入下一轮 completion。
3. 以为 Responses API 一旦支持工具,就所有模式都兼容#
源码对 MCP + background/streaming 组合有明确限制。
如果 built-in tool 回路不工作,先怎么查#
建议按这个顺序:
- 当前请求是不是 harmony 路径,而不是
SimpleContext - system/developer message 里是否真的注入了工具描述
- 最后一条 message 的
recipient是否正确指向browser.*或python tool_session是否成功建立- 工具结果是否被重新 append 成 message,并被
render_for_completion()送回下一轮
如果模型根本不发 tool recipient,问题更可能在 prompt/description 层;如果发了但工具没执行,问题更可能在 context/session 层;如果工具执行了但后续生成没接上,问题更可能在 append/render 的上下文桥。
小结#
这一章真正要补齐的,是结构化生成里最像“工作流编排器”的一条隐藏主线:
ConversationContext让 built-in tool 回路有了正式骨架- harmony message 的
recipient决定是否进入工具分支 - tool result 会被重新编入上下文,再触发下一轮生成
- 因而 built-in tool 不是输出后处理,而是生成循环的一部分
把这层读清楚之后,你再看 Responses、tool server 和 parser,就不会再把它们误读成几组松散拼接的功能点。
叶王 © 2013-2026 版权所有。如果本文档对你有所帮助,可以请作者喝饮料。