一、那个让你想把显示器关掉的场景
凌晨两点,你让 AI Agent "给订单系统写一个接口测试脚本,覆盖查询和退款两个接口"。三分钟后,它洋洋洒洒回了 200 行 Python,看着挺像那么回事。你一跑,满屏 NameError 和 KeyError。你把报错贴回去,它道歉、改了一版变量名,错误原封不动。又贴回去,它开始给你讲测试金字塔。
三个小时过去,Agent 在聊天框里演了一场独角戏,你的 Jira 进度还是 0。
这不是模型不够聪明,是你把它当成了一只"会说话的鹦鹉",却指望它自己伸手开柜子、拿文件、用工具、改错误。2026 年的 AI 落地早已过了"接个 API 就能 demo"的阶段,现在拼的是:怎么把大模型从"一次性回答"变成"能闭环干活的 Agent"。
这篇文章不聊玄学,直接给你一个在生产环境验证过的模式------ReAct(Reasoning + Acting)循环,再加一个可插拔的工具注册表。读完你能用 200 行 Python 搭一个骨架,让国产大模型(DeepSeek / 通义 / 文心 / GLM / Kimi / 豆包都能接)真的会查数据库、调接口、看报错、再重试。
二、问题出在哪:不是模型笨,是你没给它"手"和"脑子"
先做一个基本判断:大模型本质上是一个"文本预测机"。你给一段 prompt,它预测下一段最可能像答案的文本。这个能力在写诗、总结、翻译上强得离谱,但在"完成一件多步骤任务"上有个致命短板------它没有状态、没有工具、没有试错循环。
看一张对比表,左边是多数团队现在的用法,右边是 ReAct Agent 落地后的用法:
|-------|------------------|--------------------|
| 维度 | 裸用 LLM(Before) | ReAct Agent(After) |
| 任务理解 | 一次性 prompt,上下文有限 | 每轮根据观察重新思考,动态调整计划 |
| 外部工具 | 没有,只能瞎编 | 显式注册工具,模型按需调用 |
| 错误处理 | 出了错继续胡扯 | 把报错喂回模型,让它自己改 |
| 多步骤任务 | 一步错,步步错 | 每步有观察、有校验、可回退 |
| 可控性 | 黑盒输出 | 中间过程可观测、可审计、可干预 |
核心差别就一句话:裸用 LLM 是在"问答案",Agent 是在"跑任务"。
再用一张图把架构差异画清楚:

三、ReAct 到底是什么:把"推理"和"行动"串成一个环
ReAct 不是某个框架的名字,是一种思想:把大模型的 "Reasoning(推理)" 和 "Acting(行动)" 交替执行,形成一个循环。
一次循环只有三步:
- Thought(思考):模型根据当前目标和已有观察,决定下一步要做什么。
- Action(行动) :模型输出一个工具调用指令,比如
query_order(order_id="A1001")。
- Observation(观察):程序执行这个工具调用,把真实结果返回给模型。
如果任务没完成,就再来一轮:基于新的观察继续思考、继续行动。直到模型认为可以给出最终答案,循环结束。
用图看更直观:

关键点:模型不直接操作外部世界,它只输出"我想调用某个工具";真正执行工具的是你的代码。这样你既保留了大模型的推理能力,又把所有外部调用收拢在可控的代码路径里。
四、200 行 Python 骨架:能直接抄的 Agent
下面这段代码不依赖 LangChain、不依赖 LlamaIndex,纯标准库 + requests。目标只有一个:让你看清楚 ReAct 循环的每一根骨头。
4.1 工具注册表
用装饰器把普通函数注册成 Agent 可调用的工具,同时自动生成 JSON Schema:
python
import inspect
import json
from typing import Callable, Any
class ToolRegistry:
def __init__(self):
self.tools: dict[str, Callable] = {}
self.specs: list[dict] = []
def register(self, description: str):
"""装饰器:把函数注册为 Agent 可调用的工具。"""
def wrapper(fn: Callable) -> Callable:
name = fn.__name__
self.tools[name] = fn
self.specs.append({
"type": "function",
"function": {
"name": name,
"description": description,
"parameters": self._build_schema(fn),
},
})
return fn
return wrapper
@staticmethod
def _build_schema(fn: Callable) -> dict:
sig = inspect.signature(fn)
props: dict[str, dict] = {}
required: list[str] = []
type_map = {int: "integer", float: "number", bool: "boolean", str: "string"}
for pname, param in sig.parameters.items():
t = param.annotation if param.annotation is not inspect.Parameter.empty else str
props[pname] = {"type": type_map.get(t, "string")}
if param.default is inspect.Parameter.empty:
required.append(pname)
return {"type": "object", "properties": props, "required": required}
def call(self, name: str, arguments: dict) -> str:
if name not in self.tools:
return json.dumps({"error": f"工具 {name} 不存在"}, ensure_ascii=False)
try:
result = self.tools[name](**arguments)
return json.dumps(result, ensure_ascii=False)
except Exception as e:
# 关键:把报错结构化地返回,让模型下一轮能自己修
return json.dumps({"error": str(e)}, ensure_ascii=False)
registry = ToolRegistry()
@registry.register(description="查询指定城市的当前天气,参数 city 为城市名")
def get_weather(city: str) -> dict:
# 生产环境这里接真实天气 API;示例用 mock
return {"city": city, "weather": "晴", "temperature": 26}
@registry.register(description="根据订单号查询订单状态,参数 order_id 为订单编号")
def query_order(order_id: str) -> dict:
mock_db = {"A1001": "已发货", "A1002": "待付款", "A1003": "已退款"}
return {"order_id": order_id, "status": mock_db.get(order_id, "未找到")}
这里有两个设计意图:
- Schema 从类型注解自动生成:少写一堆 JSON,新增工具只要写普通 Python 函数。
- 工具报错必须返回给模型:如果执行失败,Agent 下一轮能基于错误信息改参数或换工具,而不是直接崩溃。
4.2 大模型调用层
用 requests 直接调 DeepSeek,兼容任意支持 OpenAI 接口格式的国产模型:
python
import os
import requests
DEEPSEEK_API_KEY = os.getenv("DEEPSEEK_API_KEY")
BASE_URL = "https://api.deepseek.com/v1"
def chat(messages: list[dict], tools: list[dict] | None = None) -> dict:
headers = {
"Authorization": f"Bearer {DEEPSEEK_API_KEY}",
"Content-Type": "application/json",
}
body = {
"model": "deepseek-chat",
"messages": messages,
"temperature": 0.3, # Agent 需要稳定,别让它太发散
}
if tools:
body["tools"] = tools
resp = requests.post(
f"{BASE_URL}/chat/completions",
headers=headers,
json=body,
timeout=60,
)
resp.raise_for_status()
return resp.json()["choices"][0]["message"]
注意 temperature 别设太高。Agent 的每一轮思考都要稳定,创意可以留给最终文案,推理步骤不能飘。
4.3 ReAct 循环
核心逻辑:解析模型输出里的 TOOL_CALL 标记,执行工具,把结果塞回上下文,继续下一轮。
python
import re
SYSTEM_PROMPT = """你是一个智能助手,解决问题时必须遵循 ReAct 模式:
1. Thought:先分析当前情况,说明你要做什么。
2. Action:如果需要工具,按格式输出 TOOL_CALL: {"name": "工具名", "arguments": {...}}。
3. Observation:工具返回会自动提供给你,用于下一轮思考。
如果已经得到足够信息,直接输出 FINAL_ANSWER: 你的最终答案。
"""
def run_agent(query: str, max_turns: int = 10) -> str:
messages = [
{"role": "system", "content": SYSTEM_PROMPT},
{"role": "user", "content": query},
]
for turn in range(max_turns):
msg = chat(messages, registry.specs)
content = msg.get("content", "") or ""
messages.append({"role": "assistant", "content": content})
# 终止条件 1:模型直接给最终答案
if "FINAL_ANSWER:" in content:
return content.split("FINAL_ANSWER:", 1)[1].strip()
# 终止条件 2:模型没调用工具也没给答案,提醒它
match = re.search(r'TOOL_CALL:\s*(\{.*?\})\s*$', content, re.DOTALL)
if not match:
messages.append({
"role": "user",
"content": "请按格式调用工具,或直接给出 FINAL_ANSWER。",
})
continue
# 执行工具调用
call = json.loads(match.group(1))
name = call.get("name")
args = call.get("arguments", {})
observation = registry.call(name, args)
# 把观察结果写回上下文
messages.append({
"role": "user",
"content": f"Observation: {observation}",
})
return "达到最大轮次,任务未完成。"
if __name__ == "__main__":
answer = run_agent("帮我查一下订单 A1001 的状态,再看看南京今天天气怎么样")
print(answer)
跑起来之后,一次典型的交互长这样:
python
Turn 1 Thought: 用户问了两个事,先查订单 A1001。
Action: TOOL_CALL: {"name":"query_order","arguments":{"order_id":"A1001"}}
Observation: {"order_id": "A1001", "status": "已发货"}
Turn 2 Thought: 订单已发货,再查南京天气。
Action: TOOL_CALL: {"name":"get_weather","arguments":{"city":"南京"}}
Observation: {"city": "南京", "weather": "晴", "temperature": 26}
Turn 3 Thought: 两个信息都有了,给出最终答案。
FINAL_ANSWER: 订单 A1001 已发货;南京今天晴,26℃。
每一轮你都能打印出来,问题卡在哪一眼就能看见。这比"黑盒生成一个答案"好调试十倍。
五、三个工程化细节,决定 Agent 能不能上生产
代码能跑只是 20%。把 Agent 搬进生产环境,下面这三个坑我踩过不止一次。
5.1 工具描述就是 prompt,写不好模型会乱调
装饰器里的 description 不是给人类看的注释,它会原封不动塞进模型上下文。如果你写:
python
@registry.register(description="查订单")
def query_order(order_id: str) -> dict:
...
模型根本搞不清 order_id 是什么格式。正确写法要包含:工具是干什么的、参数格式、返回什么、什么时候不该用。
python
@registry.register(
description="根据订单号查询订单状态。参数 order_id 为大写字母+数字组合,"
"例如 A1001。仅用于回答与订单状态相关的问题。"
)
def query_order(order_id: str) -> dict:
...
这个描述会占用上下文 token,太啰嗦费钱,太简单误调。需要在真实问题上反复测。
5.2 观察结果要截断、要结构化
真实 API 返回可能是几百 KB 的 JSON。如果你把整个 JSON 原样塞回 prompt,几轮之后上下文就爆炸了。生产上要做两件事:
- 截断:只把模型需要的字段抽出来返回。
- 结构化:统一用 JSON,别让模型去猜自然语言里的关键信息。
例如天气接口返回了 30 个字段,Agent 其实只需要 temperature 和 weather。
5.3 循环必须有上限和兜底
max_turns 不是摆设。模型可能在某个循环里反复调用同一个工具,陷入死循环。上线前必须配:
- 最大轮次(例如 10)。
- 单工具重复调用检测(同一工具同一参数连续 3 次,强制退出)。
- 兜底答案:循环耗尽后返回一句"系统无法完成,请人工介入"。
六、Before / After:效果到底差多少?
回到开篇那个"写接口测试脚本"的场景。用 ReAct Agent 之后,流程会变成:
- Agent 先读项目目录,识别测试框架(pytest)。
- 调用文件读取工具看已有接口封装。
- 生成脚本。
- 调用本地 shell 工具执行 pytest。
- 观察报错,把报错塞回上下文,改脚本。
- 直到 pytest 通过,给出最终脚本。
对比效果:
|-----------|----------------|------------------------------------|
| 指标 | 裸用 LLM | ReAct Agent |
| 多步骤任务完成率 | 30%~40% | 80%~90% |
| 遇到报错能否自修复 | 不能,越改越偏 | 能,把报错当观察喂回去 |
| 工具调用可控性 | 无 | 白名单 + 审计日志 |
| 新增业务能力成本 | 改 prompt,提示词工程 | 写一个 Python 函数 |
| 可观测性 | 黑盒 | 每轮 Thought/Action/Observation 都可打印 |
七、结语
2026 年的 AI 竞争,已经从"谁的模型参数更大"转向了"谁能把模型更安全、更稳定、更可维护地接到真实业务里"。ReAct 不是银弹,但它解决了一个最基础的问题:让大模型从"说答案"变成"做事情"。
这个 200 行的骨架可以扩展成很多形态:接 MCP Server、接 Playwright 做 UI 测试、接数据库查业务数据、接代码仓库做自动化重构。核心套路不变------思考、行动、观察、再思考。
拿去跑一遍,把 get_weather 换成你的真实业务接口,你就知道 Agent 和聊天机器人的区别了。
参考与备注
- ReAct 论文:Yao et al., ReAct: Synergizing Reasoning and Acting in Language Models, ICLR 2023.
- 工具调用(Function Calling)基础概念:OpenAI / DeepSeek / 智谱 / 阿里云官方 API 文档。
- MCP(Model Context Protocol)协议:可作为 ReAct Agent 工具层的进一步标准化方向,参考 https://modelcontextprotocol.io/specification/。
- 本文示例基于 Python 3.12 + DeepSeek API 编写,替换
BASE_URL和model即可接入其他兼容 OpenAI 接口的国产大模型。