大模型决定调用某个工具只是整个流程的一半,真正决定应用效果的是工具执行结果如何回到模型手里。不少团队在接入推理API时,把Function Call的参数解析做得很完善,却在结果回传这一步踩坑:有人把结果直接拼进用户消息,有人忘记填tool_call_id导致接口直接报错,有人在多轮循环调用中把上下文塞得越来越大,最终token费用失控。本文以主流推理API的通用规范为基础,把结果注入推理上下文的机制讲清楚,并给出可直接落地的请求示例。

一、结果回传的基本消息结构:role为tool的消息
当模型返回一条带tool_calls的assistant消息后,客户端执行完工具,需要把结果组装成一条新的消息追加到对话历史中,再次发起请求。这条消息的核心特征是角色为tool,而不是常见的user或assistant。以广泛兼容的Chat Completions风格接口为例,回传消息包含三个关键字段:role固定为tool,tool_call_id必须与模型上次返回的某个工具调用id完全一致,content承载工具执行的结果文本。
一个典型的完整请求体如下,注意assistant消息要原样保留在历史里,否则模型无法对应它当初发起的调用:
{
"model": "your-model-name",
"messages": [
{
"role": "user",
"content": "帮我查一下北京今天的气温"
},
{
"role": "assistant",
"content": null,
"tool_calls": [
{
"id": "call_abc123",
"type": "function",
"function": {
"name": "get_weather",
"arguments": "{\"city\":\"北京\"}"
}
}
]
},
{
"role": "tool",
"tool_call_id": "call_abc123",
"content": "{\"temperature\": 18, \"condition\": \"晴\"}"
}
],
"tools": [
{
"type": "function",
"function": {
"name": "get_weather",
"description": "查询指定城市当前天气",
"parameters": {
"type": "object",
"properties": {
"city": {"type": "string"}
},
"required": ["city"]
}
}
}
]
}这里有几个容易出错的细节。第一,tool_call_id是模型侧生成的随机标识,不是你自己编的,必须从上一轮响应里原样复制,多一个空格都会导致400错误。第二,assistant消息中的tool_calls数组在回传时要完整保留,哪怕content为null也不能删掉这条消息,因为它是模型判断上下文连续性的依据。第三,工具结果不一定非要是纯文本,多数接口支持JSON字符串,结构化数据反而更利于模型准确提取字段。
二、多工具并行调用的结果组织与匹配
能力较强的模型经常在一次响应中同时发起多个工具调用,比如用户问“对比上海和深圳的房价”,模型可能返回两个tool_calls,分别查询两个城市。这种情况下,回传时必须为每一个调用id都提供一条对应的tool消息,不能合并成一条。各厂商接口对此要求略有差异,有的允许把多条结果合并到一个消息里用数组表达,有的则严格一一对应,采用一一对应的写法兼容性最好。
{
"role": "tool",
"tool_call_id": "call_001",
"content": "{\"city\": \"上海\", \"avg_price\": 58200}"
},
{
"role": "tool",
"tool_call_id": "call_002",
"content": "{\"city\": \"深圳\", \"avg_price\": 61500}"
}除了并行调用,还有一种更复杂的情况是循环调用:模型拿到第一批结果后,发现信息不足,会再次发起tool_calls。这实际上构成了一个“模型发起调用、客户端执行、结果回传、模型再决策”的循环。工程上建议用一个while循环实现,终止条件是模型返回的finish_reason为stop且不再包含tool_calls。每次循环都要把新产生的assistant消息和tool消息追加进messages数组,而不是替换,模型的推理依赖完整的调用链条。
循环调用还有一个隐蔽的坑:如果工具执行失败,比如数据库连接超时,千万不要直接中断流程,也不建议返回空字符串。正确做法是把错误信息如实写进tool消息的content,例如返回“查询超时,请稍后重试”这样的描述,让模型有机会决定是换个参数重试还是向用户说明情况。直接抛异常中断会让模型失去决策机会,用户侧表现就是对话莫名其妙结束。
三、上下文膨胀治理与结果裁剪策略
结果回传机制天然会导致消息历史快速增长。假设一个数据分析Agent每轮调用返回几KB的查询结果,十几轮对话后上下文轻松突破模型窗口限制,即使没有超限,token费用也相当可观。治理思路主要有三种,可以组合使用。
第一种是结果裁剪。工具返回的数据往往远超模型实际需要的部分,比如查订单列表返回了50个字段,模型只需要订单号和状态。可以在客户端加一层裁剪逻辑,只把相关字段回传。第二种是摘要压缩,对于长文本类结果,可以先调用一次轻量模型做摘要,再把摘要作为tool消息回传。第三种是滑动窗口,当历史超过阈值时,把早期的工具结果替换为一句简短的占位描述,例如“此前已查询过订单列表,结果已省略”。需要注意的是,最近一轮的tool消息不要省略,否则模型会因为缺失关键信息而重复调用工具。
def trim_tool_result(result: dict, keep_fields: list) -> str:
# 只保留模型需要的字段,减少回传token
trimmed = {k: result.get(k) for k in keep_fields if k in result}
return json.dumps(trimmed, ensure_ascii=False)
# 使用示例:订单查询结果只回传关键字段
raw = {"order_id": "A88231", "status": "已发货", "raw_log": "..." * 2000}
msg = {
"role": "tool",
"tool_call_id": "call_009",
"content": trim_tool_result(raw, ["order_id", "status"])
}最后补充一点工程实践建议:给整个调用链路加上请求日志,记录每次tool_call_id、执行耗时和结果长度,排查线上问题时这些日志能帮你快速定位到底是模型没发起调用、工具执行出错,还是结果格式不对。同时建议对content长度做上限保护,超过阈值的工具结果强制截断并附加提示,避免一次异常的超大返回把整个会话拖垮。把结果回传这一环做扎实,Function Call才能真正形成可靠的业务闭环。
Function Call推理API上下文注入修改时间:2026-09-14 14:37:01