文章目录
- 1、底层机制
-
- [1.1 调用测试](#1.1 调用测试)
参考 https://www.zhihu.com/question/1979245388159537776/answer/2029860455544513217
1、底层机制
需要搞清楚三件事:
Function Calling 到底是怎么工作的。 LLM 不是真的在"调用"工具,它只是根据你给的 schema 描述,输出了一段 JSON。你的代码解析这个 JSON,去调真实的函数,把结果塞回对话,模型再继续。整个过程里,模型完全依赖你写的 schema 描述来理解每个工具的用途和参数。描述写得含糊,模型就传错参数;参数类型没说清楚,模型就传错类型。这不是玄学,是工程问题。
ReAct 循环是什么,以及它在哪里会出问题。 现代 Agent 框架底层几乎都是 ReAct:Thought(我要干什么)→ Action(调工具)→ Observation(看结果)→ 再 Thought。知道这个循环之后,你才能理解 Agent 的常见失败模式、死循环(一直 Observation 不满意,一直重试)、Context 爆炸(循环轮次太多,历史塞满上下文)、早停(模型在还没拿到关键信息时就判断"任务完成")。
Token 和 Context Window 的物理限制。 Agent 的 Context 不是无限的。GPT-4o 的 context window 是 128k token,Qwen 系列不同版本在 8k 到 128k 之间。一个 ReAct 循环跑 10 轮,每轮塞进 Thought + Action + Observation,很快就能把 context 吃满。吃满之后发生什么?模型开始"遗忘"早期的内容,行为变得不可预测。这就是为什么 Memory 管理是 Agent 工程的核心问题之一。这三件事,不需要读论文,自己动手写一个 50 行的最小 Agent,不用框架,直接调 OpenAI API,手动解析 function calling 的返回,手动拼 messages 数组,花一个下午就能把这些机制摸清楚。
1.1 调用测试
问答一
最大3轮
curl -s -X POST http://127.0.0.1:8092/api/agent/chat \
-H 'Content-Type: application/json' \
-d '{"message":"严格按顺序、每步只调用一次add_numbers,不要心算:①1+1 ②把上步结果+1 ③再+1 ④再+1 ⑤再+1 ⑥再+1。共6次加法,每次等Observation再继续。"}'
{"code":1,"message":"达到最大 ReAct 轮次 3(可能发生死循环或 context 即将爆炸)"}%
日志:
2026/05/26 17:13:41.362642 agent.go:55: [INFO][ReAct] 开始 maxTurns=3 messages=2 user="严格按顺序、每步只调用一次add_numbers,不要心算:①1+1 ②把上步结果+1 ③再+1 ④再+1 ⑤再..."
2026/05/26 17:13:41.362691 agent.go:59: [INFO][ReAct turn=1/3] >>> 发起 LLM 请求(当前 messages=2)
2026/05/26 17:13:48.810440 agent.go:78: [INFO][ReAct turn=1/3] <<< LLM 返回 finish_reason="tool_calls" tool_calls=1 thought="我将严格按照您的要求,逐步进行加法运算。\n\n① **步骤 1**:1 + 1"
2026/05/26 17:13:48.810709 agent.go:142: [INFO][ReAct turn=1/3] 提示: finish_reason=tool_calls 表示模型本轮输出的是工具调用;执行工具后由应用发起下一轮,不等于任务完成
2026/05/26 17:13:48.810762 agent.go:108: [INFO][ReAct turn=1/3] Action id=chatcmpl-tool-1117b4dce3414128a0098bf893c3925d: add_numbers({"a": 1, "b": 1})
2026/05/26 17:13:48.810850 agent.go:117: [INFO][ReAct turn=1/3] Observation: 2
2026/05/26 17:13:48.810897 agent.go:126: [INFO][ReAct turn=1/3] 本轮结束(API finish_reason="tool_calls"),工具结果已写入 messages,messages=4
2026/05/26 17:13:48.810929 agent.go:130: [INFO][ReAct turn=1/3] 应用继续:即将进入 turn=2
2026/05/26 17:13:48.810961 agent.go:59: [INFO][ReAct turn=2/3] >>> 发起 LLM 请求(当前 messages=4)
2026/05/26 17:13:51.530615 agent.go:78: [INFO][ReAct turn=2/3] <<< LLM 返回 finish_reason="tool_calls" tool_calls=1 thought=""
2026/05/26 17:13:51.530760 agent.go:142: [INFO][ReAct turn=2/3] 提示: finish_reason=tool_calls 表示模型本轮输出的是工具调用;执行工具后由应用发起下一轮,不等于任务完成
2026/05/26 17:13:51.530804 agent.go:108: [INFO][ReAct turn=2/3] Action id=call_3a5fbbc2be3d46a0a39c3a22: add_numbers({"a":2,"b":1})
2026/05/26 17:13:51.530869 agent.go:117: [INFO][ReAct turn=2/3] Observation: 3
2026/05/26 17:13:51.530910 agent.go:126: [INFO][ReAct turn=2/3] 本轮结束(API finish_reason="tool_calls"),工具结果已写入 messages,messages=6
2026/05/26 17:13:51.530942 agent.go:130: [INFO][ReAct turn=2/3] 应用继续:即将进入 turn=3
2026/05/26 17:13:51.530968 agent.go:59: [INFO][ReAct turn=3/3] >>> 发起 LLM 请求(当前 messages=6)
2026/05/26 17:13:53.868552 agent.go:78: [INFO][ReAct turn=3/3] <<< LLM 返回 finish_reason="tool_calls" tool_calls=1 thought=""
2026/05/26 17:13:53.868623 agent.go:142: [INFO][ReAct turn=3/3] 提示: finish_reason=tool_calls 表示模型本轮输出的是工具调用;执行工具后由应用发起下一轮,不等于任务完成
2026/05/26 17:13:53.868635 agent.go:108: [INFO][ReAct turn=3/3] Action id=chatcmpl-tool-c66ca257e42e434cbd44d29016e70edd: add_numbers({"a": 3, "b": 1})
2026/05/26 17:13:53.868652 agent.go:117: [INFO][ReAct turn=3/3] Observation: 4
2026/05/26 17:13:53.868666 agent.go:126: [INFO][ReAct turn=3/3] 本轮结束(API finish_reason="tool_calls"),工具结果已写入 messages,messages=8
2026/05/26 17:13:53.868674 agent.go:134: [ERROR][ReAct] 达到 maxTurns=3 仍未 stop,最后一轮 finish_reason 多为 tool_calls
问答二
最大8轮,
curl -s -X POST http://127.0.0.1:8092/api/agent/chat \
-H 'Content-Type: application/json' \
-d '{"message":"严格按顺序、每步只调用一次add_numbers,不要心算:①1+1 ②把上步结果+1 ③再+1 ④再+1 ⑤再+1 ⑥再+1。共6次加法,每次等Observation再继续。"}'
{"code":0,"message":"ok","data":{"sessionId":"2cf806cb578c59f6652c4feeb767a6a6","reply":"The resultof performing the six successive additions as requested is:\n\n**7**.","trace":[{"turn":1,"finishReason":"tool_calls","actions":["add_numbers({\"a\": 1, \"b\": 1})"],"observations":["2"]},{"turn":2,"finishReason":"tool_calls","actions":["add_numbers({\"a\":2,\"b\":1})"],"observations":["3"]},{"turn":3,"finishReason":"tool_calls","actions":["add_numbers({\"a\": 3, \"b\": 1})"],"observations":["4"]},{"turn":4,"finishReason":"tool_calls","thought":"We need 6 additions total. We did 3. Need three more: 4+1, 5+1, 6+1? Wait start 1+1=2, then +1=3, +1=4, +1=5, +1=6, +1=7. So after 3 we have 4. Need 4,5,6? Actually step counts: 1:2, 2:3, 3:4, 4:5, 5:6, 6:7. So currently at step3 result 4. Need step4 4+1=5.","actions":["add_numbers({\"a\":4,\"b\":1})"],"observations":["5"]},{"turn":5,"finishReason":"tool_calls","actions":["add_numbers({\"a\": 5, \"b\": 1})"],"observations":["6"]},{"turn":6,"finishReason":"tool_calls","actions":["add_numbers({\"a\": 6, \"b\": 1})"],"observations":["7"]},{"turn":7,"finishReason":"stop","thought":"The resultof performing the six successive additions as requested is:\n\n**7**."}]}}%
日志:
2026/05/26 17:29:26.186839 agent.go:55: [INFO][ReAct] 开始 maxTurns=8 messages=2 user="严格按顺序、每步只调用一次add_numbers,不要心算:①1+1 ②把上步结果+1 ③再+1 ④再+1 ⑤再..."
2026/05/26 17:29:26.186888 agent.go:59: [INFO][ReAct turn=1/8] >>> 发起 LLM 请求(当前 messages=2)
2026/05/26 17:30:34.179419 agent.go:78: [INFO][ReAct turn=1/8] <<< LLM 返回 finish_reason="tool_calls" tool_calls=1 thought=""
2026/05/26 17:30:34.181079 agent.go:142: [INFO][ReAct turn=1/8] 提示: finish_reason=tool_calls 表示模型本轮输出的是工具调用;执行工具后由应用发起下一轮,不等于任务完成
2026/05/26 17:30:34.181127 agent.go:108: [INFO][ReAct turn=1/8] Action id=chatcmpl-tool-8f916876a1adeb04: add_numbers({"a": 1, "b": 1})
2026/05/26 17:30:34.181180 agent.go:117: [INFO][ReAct turn=1/8] Observation: 2
2026/05/26 17:30:34.181204 agent.go:126: [INFO][ReAct turn=1/8] 本轮结束(API finish_reason="tool_calls"),工具结果已写入 messages,messages=4
2026/05/26 17:30:34.181224 agent.go:130: [INFO][ReAct turn=1/8] 应用继续:即将进入 turn=2
2026/05/26 17:30:34.181244 agent.go:59: [INFO][ReAct turn=2/8] >>> 发起 LLM 请求(当前 messages=4)
2026/05/26 17:30:48.294897 agent.go:78: [INFO][ReAct turn=2/8] <<< LLM 返回 finish_reason="tool_calls" tool_calls=1 thought=""
2026/05/26 17:30:48.295114 agent.go:142: [INFO][ReAct turn=2/8] 提示: finish_reason=tool_calls 表示模型本轮输出的是工具调用;执行工具后由应用发起下一轮,不等于任务完成
2026/05/26 17:30:48.295156 agent.go:108: [INFO][ReAct turn=2/8] Action id=call_d994d1d001a94f45ac3d6c2b: add_numbers({"a":2,"b":1})
2026/05/26 17:30:48.295204 agent.go:117: [INFO][ReAct turn=2/8] Observation: 3
2026/05/26 17:30:48.295282 agent.go:126: [INFO][ReAct turn=2/8] 本轮结束(API finish_reason="tool_calls"),工具结果已写入 messages,messages=6
2026/05/26 17:30:48.295313 agent.go:130: [INFO][ReAct turn=2/8] 应用继续:即将进入 turn=3
2026/05/26 17:30:48.295336 agent.go:59: [INFO][ReAct turn=3/8] >>> 发起 LLM 请求(当前 messages=6)
2026/05/26 17:30:52.025371 agent.go:78: [INFO][ReAct turn=3/8] <<< LLM 返回 finish_reason="tool_calls" tool_calls=1 thought=""
2026/05/26 17:30:52.026655 agent.go:142: [INFO][ReAct turn=3/8] 提示: finish_reason=tool_calls 表示模型本轮输出的是工具调用;执行工具后由应用发起下一轮,不等于任务完成
2026/05/26 17:30:52.026698 agent.go:108: [INFO][ReAct turn=3/8] Action id=chatcmpl-tool-ef597064fb23496f9040a6f80a1e7f15: add_numbers({"a": 3, "b": 1})
2026/05/26 17:30:52.026756 agent.go:117: [INFO][ReAct turn=3/8] Observation: 4
2026/05/26 17:30:52.026795 agent.go:126: [INFO][ReAct turn=3/8] 本轮结束(API finish_reason="tool_calls"),工具结果已写入 messages,messages=8
2026/05/26 17:30:52.026822 agent.go:130: [INFO][ReAct turn=3/8] 应用继续:即将进入 turn=4
2026/05/26 17:30:52.026849 agent.go:59: [INFO][ReAct turn=4/8] >>> 发起 LLM 请求(当前 messages=8)
2026/05/26 17:30:58.401652 agent.go:78: [INFO][ReAct turn=4/8] <<< LLM 返回 finish_reason="tool_calls" tool_calls=1 thought="We need 6 additions total. We did 3. Need three more: 4+1, 5+1, 6+1? Wait start 1+1=2, then +1=3, +1=4, +1=5, +1=6, +1=7. So after 3 we have 4. Need 4,5,6? Actu..."
2026/05/26 17:30:58.401829 agent.go:142: [INFO][ReAct turn=4/8] 提示: finish_reason=tool_calls 表示模型本轮输出的是工具调用;执行工具后由应用发起下一轮,不等于任务完成
2026/05/26 17:30:58.401871 agent.go:108: [INFO][ReAct turn=4/8] Action id=chatcmpl-tool-a14cf65d9c57f12a: add_numbers({"a":4,"b":1})
2026/05/26 17:30:58.401928 agent.go:117: [INFO][ReAct turn=4/8] Observation: 5
2026/05/26 17:30:58.401963 agent.go:126: [INFO][ReAct turn=4/8] 本轮结束(API finish_reason="tool_calls"),工具结果已写入 messages,messages=10
2026/05/26 17:30:58.402004 agent.go:130: [INFO][ReAct turn=4/8] 应用继续:即将进入 turn=5
2026/05/26 17:30:58.402037 agent.go:59: [INFO][ReAct turn=5/8] >>> 发起 LLM 请求(当前 messages=10)
2026/05/26 17:31:07.207916 agent.go:78: [INFO][ReAct turn=5/8] <<< LLM 返回 finish_reason="tool_calls" tool_calls=1 thought=""
2026/05/26 17:31:07.207956 agent.go:142: [INFO][ReAct turn=5/8] 提示: finish_reason=tool_calls 表示模型本轮输出的是工具调用;执行工具后由应用发起下一轮,不等于任务完成
2026/05/26 17:31:07.207962 agent.go:108: [INFO][ReAct turn=5/8] Action id=chatcmpl-tool-b60f0c7b31a10a54: add_numbers({"a": 5, "b": 1})
2026/05/26 17:31:07.207972 agent.go:117: [INFO][ReAct turn=5/8] Observation: 6
2026/05/26 17:31:07.207981 agent.go:126: [INFO][ReAct turn=5/8] 本轮结束(API finish_reason="tool_calls"),工具结果已写入 messages,messages=12
2026/05/26 17:31:07.207986 agent.go:130: [INFO][ReAct turn=5/8] 应用继续:即将进入 turn=6
2026/05/26 17:31:07.207992 agent.go:59: [INFO][ReAct turn=6/8] >>> 发起 LLM 请求(当前 messages=12)
2026/05/26 17:31:08.110691 agent.go:78: [INFO][ReAct turn=6/8] <<< LLM 返回 finish_reason="tool_calls" tool_calls=1 thought=""
2026/05/26 17:31:08.110784 agent.go:142: [INFO][ReAct turn=6/8] 提示: finish_reason=tool_calls 表示模型本轮输出的是工具调用;执行工具后由应用发起下一轮,不等于任务完成
2026/05/26 17:31:08.110816 agent.go:108: [INFO][ReAct turn=6/8] Action id=call_b1cc9e8c26484a3cba0e4789: add_numbers({"a": 6, "b": 1})
2026/05/26 17:31:08.110857 agent.go:117: [INFO][ReAct turn=6/8] Observation: 7
2026/05/26 17:31:08.110889 agent.go:126: [INFO][ReAct turn=6/8] 本轮结束(API finish_reason="tool_calls"),工具结果已写入 messages,messages=14
2026/05/26 17:31:08.110915 agent.go:130: [INFO][ReAct turn=6/8] 应用继续:即将进入 turn=7
2026/05/26 17:31:08.110941 agent.go:59: [INFO][ReAct turn=7/8] >>> 发起 LLM 请求(当前 messages=14)
2026/05/26 17:31:08.894673 agent.go:78: [INFO][ReAct turn=7/8] <<< LLM 返回 finish_reason="stop" tool_calls=0 thought="The resultof performing the six successive additions as requested is:\n\n**7**."
2026/05/26 17:31:08.894855 agent.go:97: [INFO][ReAct turn=7/8] 早停(stop),Agent 任务结束
2026/05/26 17:31:08.894943 agent.go:99: [INFO][ReAct] 结束 总轮次=7 trace_steps=7 reply_len=77
第一件事:Function Calling 到底怎么工作
看 log 里成对的两行(每一轮都一样):
ReAct turn=1\] Action: add_numbers({"a":1,"b":1}) \[ReAct turn=1\] Observation: 2 对应代码路径: 模型只返回一段 JSON 意图:{"a":1,"b":1}(包在 tool_calls 里),没有执行加法。 executeTool(tools.go)解析 JSON,在 Go 里算 1+1,得到 "2"。 Observation 是你写进 messages 的 role: tool 消息,模型下一轮才能「看见」2。 **模型只描述要调什么,参数是什么。 Observation 必须是你的代码返回的字符串** 第二件事:ReAct 循环是什么、在哪里出问题 这次是典型的 Action → Observation → 再 Action 链,且没有在中间 stop: turn=1: 1+1 → 2 turn=2: 2+1 → 3 (用了上一步 Observation) turn=3: 3+1 → 4 turn=4: 4+1 → 5 turn=5: 本应有 5+1 → 6,但 for 循环不允许第 5 次请求 → 直接报错返回 第三件事:Token / Context Window 的物理限制 这次还没碰到模型 context 上限,碰到的是你们自己设的 MaxTurns。 但同一次请求里,context 已经在悄悄变长。每多一轮,messages 大致多: 1 条 assistant(含 tool_calls) 1 条 tool(Observation 文本) 4 轮后大约是:system + user + 4×(assistant + tool) ≈ 10 条消息。若跑到 10 轮 ReAct,就是 20+ 条,再加每轮 Thought 文本,token 线性涨------这就是「10 轮 ReAct 很快吃满 context」的原因。 三件事 → 你这次 log 的一张对照表  Function Calling:只发 {"message":"1+1,必须用 add_numbers"},看 1 条 Action/Observation。 ReAct:同一道 6 步加法,AGENT_MAX_TURNS=10,对比 log 条数与 trace。 Context / 护栏:AGENT_MAX_TURNS=3 再跑 6 步题,确认又是 达到最大 ReAct 轮次 3------分清工程护栏和模型 context 两种「装不下」。 一个简单的接收大模型返回的结构: ```go type chatResponse struct { Choices []struct { Message struct { Role string `json:"role"` Content *string `json:"content"` ToolCalls []toolCall `json:"tool_calls"` } `json:"message"` FinishReason string `json:"finish_reason"` } `json:"choices"` Error *struct { Message string `json:"message"` } `json:"error"` } { "choices": [{ "message": { "role": "assistant", "content": null, "tool_calls": [{ "id": "call_xxx", "function": { "name": "add_numbers", "arguments": "{\"a\":1,\"b\":1}" } }] }, "finish_reason": "tool_calls" }] } ``` 生产里还常会加:usage(token 用量)、id、model 等 ```go var toolSchemas = []map[string]interface{}{ { "type": "function", "function": map[string]interface{}{ "name": "add_numbers", "description": "将两个数字相加。参数必须是 number 类型。", // ... }, }, ``` Agent 工程师在应用里写死的「工具说明书」。 对应的真实函数在 executeTool() 里,也是工程师写的。 大模型只读这份 schema,决定「要不要调、调哪个、参数 JSON 长什么样」;不会在你的服务器上执行 Go 代码。 为啥要把 tools 和 messages 都传给大模型? 参数 作用 messages 对话历史:谁说了什么(含过去的 tool 结果) tools 当前「可选工具菜单」:名字、描述、参数类型 每一轮 ReAct,模型需要: 看 messages → 用户要什么、上一步 Observation 是 2 还是 3; 看 tools → 我能不能用 add_numbers、参数要 a/b 两个 number。 不传 tools,模型就不知道可以 Function Calling(或只能纯聊天)。 messages 里为啥要有 system + user? ```go messages := []chatMessage{ {Role: "system", Content: strPtr(systemPrompt)}, {Role: "user", Content: strPtr(msg)}, } ```   Prompt 是啥?谁写? Prompt = 塞进 messages 里的文本,用来约束模型行为。 System prompt:工程师写(如 systemPrompt),通常每次请求都带。 User prompt:用户输入。 还可以有 few-shot、RAG 检索结果等,也是工程师决定是否插入 messages。 模型不会自动知道「你必须用工具、必须 ReAct」------要靠 system(和 tools schema)告诉它。 ```go body := map[string]interface{}{ "model": s.Model, "messages": messages, "tools": toolSchemas, } ```  常见可选参数(真实 Agent 经常用,本示例没写): temperature、top_p:随机性 max_tokens:单次生成上限 tool_choice:auto / required / 指定某个工具 stream: true:流式输出 response_format:JSON 模式等 Anthropic、Gemini 等 API 字段名可能不同(如 tools vs tool_config),但思想一样:输入 = 模型 id + 对话 + 可选工具定义。 OpenRouter 只是转发到各家,兼容 OpenAI 这一套,所以我们用这 3 个字段。  模型内部限制 模型 API(OpenRouter / OpenAI 兼容) 应用程序(Agent 工程师写的) 轮次到顶先停 userMsg POST model,messages,tools choices message tool 结果 append 到 messages 历史太长会截断/报错 systemPrompt / toolSchemas ReAct 循环 for turn... executeTool 真实函数 MaxTurns / 日志 / HTTP API 大模型:读 messages+tools,输出文本或 tool_calls JSON Context Window 最大 token User Function Calling:模型只产出「调用意图」JSON;执行在 App 里(tools.go + agent.go)。 ReAct:App 里的 while/for;模型无「ReAct 模式」开关,是你用多轮请求 + system 拼出来的行为。 Token / Context:上限在模型/API;怎么省 context(截断、摘要、少轮次)在 App(你还有 MaxTurns 这种比 context 更粗的护栏)。 一次 ReAct 轮次里谁干什么(对应你 log) \[你的服务\] 组装 messages + tools → HTTP POST \[大模型\] 返回 tool_calls: add_numbers({"a":2,"b":1}) \[你的服务\] executeTool → Observation: 3 → append 到 messages \[你的服务\] turn++,再 POST ... \[你的服务\] turn \> MaxTurns → 报错(你遇到的 4 轮上限)  OpenRouter 你的服务 OpenRouter 你的服务 turn=1 messages finish_reason=tool_calls executeTool → Observation turn=2 messages(含 tool 结果) finish_reason=tool_calls executeTool turn=3 messages finish_reason=stop, content 可能为空 return reply