# LLM 工具调用：实现完整的请求与结果循环

> 保留工具调用 ID，校验参数，处理流式片段与重复动作，搭建可验证的工具往返流程，并统计完整任务成本。

_发布 2026-09-29 · 更新 2026-09-29 · [KeepRouter Editorial](https://keeprouter.com/editorial-policy#editorial-team) · 5 分钟阅读_

![展示安全重试、回退、取消与副作用边界的 LLM 请求状态机](https://keeprouter.com/editorial/blog/llm-failover-design-guide.png)

_工作流调用工具或恢复异常时，分别记录尝试与结果。概念示意图。_

工具调用是模型请求应用执行一个指定操作。应用校验参数和权限，执行后把结果与原调用 ID 关联返回，模型才能继续回答或请求下一步。只提供工具定义，并没有完成整个流程。

例如查询库存，应该验证库存结果确实进入最终答案。“我去查库存”这句话本身不是工具调用。

## 保留完整会话协议

[OpenAI 工具调用文档](https://developers.openai.com/api/docs/guides/function-calling)定义 Chat Completions 的调用与结果消息；[Claude 停止原因说明](https://platform.claude.com/docs/en/build-with-claude/handling-stop-reasons)描述另一种协议。不同协议的消息块不能直接混用。

| 步骤 | 应用负责什么 |
| --- | --- |
| 定义 | 提供有限工具及参数结构 |
| 接收 | 保存助手消息、调用 ID 和完整参数 |
| 校验 | 检查类型、范围和用户权限 |
| 执行 | 调用允许的实现 |
| 返回 | 用原 ID 关联结果 |
| 继续 | 获得答案或有上限的后续动作 |

不要丢弃助手的工具调用消息，只发送一个孤立结果。多个调用也要分别保留身份。

## 先在本地验证参数

下面模拟一个只读库存调用，不发网络请求：

```python
import json

stock = {"DEMO-A": 7, "DEMO-B": 0}
call = {
    "id": "call_demo_1",
    "function": {"name": "lookup_stock", "arguments": '{"sku":"DEMO-A"}'},
}
if call["function"]["name"] != "lookup_stock":
    raise ValueError("Unknown tool")
args = json.loads(call["function"]["arguments"])
if not isinstance(args, dict) or set(args) != {"sku"}:
    raise ValueError("Invalid arguments")
if not isinstance(args["sku"], str) or args["sku"] not in stock:
    raise ValueError("Unknown SKU")
result_message = {
    "role": "tool",
    "tool_call_id": call["id"],
    "content": json.dumps({"sku": args["sku"], "quantity": stock[args["sku"]]}),
}
print(result_message)
```

真实后续请求中，还需包含前面的助手调用消息。业务系统也要检查当前用户是否有权查看这个库存，不能只校验 JSON。

## 等待流式参数完整

参数可能分片到达。`{"sku":"DE` 这样的片段不能解析，更不能触发动作。按协议以调用身份或索引组装，完整后再验证。

不要简单数大括号。字符串中可能包含括号，多次调用也可能交错。测试拆开的字符串、多个调用和中途断流；不完整的参数不能靠猜测补齐。

## 调用 ID 与业务去重 ID 不同

API call ID 用于关联会话；业务 operation ID 用于避免重复副作用。模型用两个不同 call ID 请求同一笔退款时，只按 call ID 去重仍可能重复执行。

带副作用的操作应使用应用生成的业务标识，并在数据库中限制状态转换。执行时再次检查权限，不把提示词阶段的判断当永久授权。先测试只读工具，再处理写入、超时和重复投递。

## 为循环设上限

规定工具轮次、总时间和预算。达到上限后返回明确的未完成状态，不继续无条件尝试。这些属于应用策略，不能假设网关替你处理。

假设允许三轮工具加一次最终回答，可能产生四次模型请求及相应工具费用。还需计入重试。重复相同参数、来回调用却没有进展，往往与状态或工具描述有关。

## 按最终结果验收

用存在的 SKU、不存在 SKU、缺货、错误参数和无权限用户组成测试。最终答案必须对应真实工具结果；结果正确但答案编造库存，仍然算失败。

在[目录](/models)和 [API 文档](/api/docs)确认型号工具能力，用[迁移检查器](/tools/api-migration-checker)核对配置，再跑无副作用完整循环。只需提取数据时，参考[结构化输出指南](/zh/blog/structured-outputs-json-mode)。

## 常见问题

### 模型发出工具调用就自动执行了吗？

应用管理的工具循环中不会。应用需要校验、执行并关联结果；托管工具可能使用另一种协议。

### 流式参数还没完整可以执行吗？

不可以。等参数完整、解析校验并授权后再执行，断流时不能猜测剩余参数。

### tool_call_id 能防止重复付款吗？

不能。它关联会话消息，副作用还需要应用自己的幂等标识及业务状态检查。

## 参考的一手资料

_本文最近复核 2026-09-29_

1. [OpenAI function calling](https://developers.openai.com/api/docs/guides/function-calling)
2. [Claude stop reasons](https://platform.claude.com/docs/en/build-with-claude/handling-stop-reasons)

## 继续阅读

- [Structured Outputs 与 JSON Mode：如何验证真实数据](https://keeprouter.com/zh/blog/structured-outputs-json-mode.md)
- [LLM 故障切换设计指南：恢复请求，同时避免不安全重试](https://keeprouter.com/zh/blog/llm-failover-design-guide.md)
- [Cline 接入 OpenAI 兼容 API：模型、工具与费用](https://keeprouter.com/zh/blog/cline-openai-compatible-api.md)

## 检查应用需要的具体行为

把文中例子用于应用时，按产品记录的请求格式与控制范围实现。

[查看产品说明](https://keeprouter.com/api/docs)

[创建 Key，测试免费模型](https://keeprouter.com/login?returnTo=%2Fconsole%2Fkeys%3Fmodel%3Dfree)

免费测试使用 free 模型；其他付费型号需要足够预付额度。

[全部文章](https://keeprouter.com/zh/blog.md) · [模型与价格](https://keeprouter.com/models.md)
