大模型 API 的本质:Render → Completion → Parse
不管你用的是 Chat Completions、Function Calling 还是 Tool Calling,在模型真正预测下一个 token 之前,所有请求都会经历三步:
- 展开(Render):把 messages、tools 定义、tool_choice 等结构化请求,渲染成一段最终的 Prompt(Token 序列)
- 补全(Completion):模型对这段 Prompt 续写下一段 Token
- 解析(Parse):把续写的原始文本还原成 assistant 文本、tool_calls 等结构化结果
换句话说:
- Chat Completions ≈ Completions + 自动把 messages 渲染成 prompt + 把输出解释回消息结构
- Chat + Tool Calling ≈ Chat + 把特定格式的补全解析成 tool_calls,并做 schema 校验
核心认知:Chat 和 Function Call 不是模型多了一种全新能力,而是服务端把 prompt 构造与输出解析自动化了。模型层面依旧只做一件事——补全下一段 token。
一个极其实用的排障方法
当 tool calling 表现异常时,不要直接调 /v1/chat/completions,而是:
- 在外部手动执行
apply_chat_template,拿到最终的 prompt 文本 - 把这个 prompt 直接丢给更底层的
/v1/completions
这样做的好处是能"看到真相":你能检查最终 Prompt 到底长什么样,还能看到模型最原始的补全文本——没有被上层 parser 二次加工或丢弃。
实际上,很多时候你甚至不需要走到协议层,只要仔细审视你发出的 API 请求本身,就能判断问题出在哪里。
三个典型的工程层 Bug
这次排障定位出的三个问题,全部发生在 Render 或 Parse 环节,与模型能力无关:
Bug 1:Prompt 末尾缺少 assistant 回合起始标记
一个参数没传递到位,导致喂给模型的 prompt 末尾少了关键的 generation prompt——相当于没告诉模型"轮到你回答了"。结果模型仍在补全 token,但完全不知道接下来该干嘛:有时续写历史对话,有时输出闲聊,有时输出半截不成形的结构化内容。
Bug 2:空内容被错误渲染成噪声
某条历史消息 content: '' 本应在 Prompt 中不产生任何输出。但框架内部为了统一数据结构,把空字符串标准化成了 [{type: "text", text: ""}] 这样的列表塞进 Prompt,直接污染了上下文,导致 tool calling 效果劣化。
Bug 3:解析器过于严格,丢弃了有效输出
模型已经生成了正确的工具调用片段,但解析器的校验太严格,把它当异常丢掉了。再加上前面上下文污染导致的格式偏差,有效输出被大量误杀。
两个关键认知
认知一:遇到问题,先还原到"Prompt 补全"层面
这个结论不仅适用于 tool calling,也适用于所有大模型 API 场景——chat、completion、structured generation 都一样。当结果不符合预期,第一步永远是:最终喂给模型的 prompt 到底长什么样?
认知二:Tool Calling 本质是"强约束 Schema 输出"
从工程角度看,tool calling 是让模型按照强约束的输出协议生成结构化片段,再由服务端解析执行。理解了这一点,你会发现 tool calling 不一定要真的调用工具——它完全可以当作 JSON 限定器或结构化生成器来用,让模型稳定产出符合 schema 的结果,再交给下游系统处理。
延伸阅读
如果对强约束生成(constrained decoding)方向感兴趣,推荐关注两个项目:
- XGrammar:把语法约束编译成高效的 token 级约束,让解码阶段就不可能走出非法分支
- LLGuidance:用约束驱动生成,把结构化正确性前置到解码过程,而非生成完再靠 parser 猜测
对于正在搭建 AI Agent 或自动化工作流的开发者来说,理解 Render → Completion → Parse 这条链路至关重要。当你的 Agent 工具调用出了问题,别急着怀疑模型能力——先检查 prompt 构造和输出解析,八成问题都在这两头。