工作台
Python 系列 / AGENT · 交互式讲义

ReAct 深剖:所有 Agent 框架都是同一个循环

给模型发消息 → 拿回结果 → 判断要不要调工具 → 执行工具、把结果塞回 messages → 再问一次 → 循环直到最终答案。这个循环叫 ReAct,2022 年就有了雏形;今天的 LangChain、LangGraph、OpenAI Agents SDK、AgentScope、Spring AI,都是这五件事的封装变体[1][2]

论文 arXiv:2210.03629(2022-10-06,Princeton + Google Brain) Agent 定义 Anthropic 官方工程博客(2024-12-19) MCP 最新规范 2026-07-28(已捐 Linux 基金会 AAIF) 本页全部出处 文末参考文献 · 核对日期 2026-09-05

01Agent 的定义:循环,不是魔法

先统一术语——"Agent"和"Workflow"的分界线,引用 Anthropic 官方工程博客《Building Effective Agents》(2024-12-19)[2]

"Workflows are systems where LLMs and tools are orchestrated through predefined code paths. Agents, on the other hand, are systems where LLMs autonomously decide how many tools to call and in what order, operating in a loop until they resolve the task."
工作流:LLM 和工具按预先写死的代码路径编排。智能体:LLM 自主决定调用多少工具、什么顺序,在循环中运行直到解决任务。
⭐ 一句话:Agent = LLM + 工具 + 循环。模型的每次回复只有两种:①「最终答案」→ 循环结束;②「我要调工具(tool_calls)」→ 你执行工具、把结果以 role="tool" 塞回 messages、再调一次模型。没有第三种情况。

02演进时间线:从论文到行业标准

每一站都有官方出处(编号对应文末参考文献):

2022-10-06
ReAct 论文发布 [1]
Yao et al.(Princeton + Google Brain)提出 Reasoning + Acting 交替:Thought → Action → Observation 文本协议,靠 prompt 约定输出格式。
2023-06-13
OpenAI 发布 function calling [3]
gpt-4-0613 / gpt-3.5-turbo-0613 支持结构化函数调用:模型返回"要调什么函数、什么参数"的 JSON,不再靠正则抠文本。
2023-11
OpenAI DevDay:tools / tool_choice 取代 functions / function_call [3][4]
现行工具调用协议定型。老教程里的 functions 写法全部过时——这是读旧资料时最大的坑。
2024-11
Anthropic 开源 MCP(Model Context Protocol)[8]
首版规范 2024-11-05:把"工具/资源"做成跨应用的标准协议(JSON-RPC 2.0,stdio / HTTP 传输)。
2024-12-19
Anthropic《Building Effective Agents》[2]
"Agent = 模型在循环中用工具完成任务"成为业界共识表述;同文提出从简单 Workflow 起步的工程原则。
2025-03-12
OpenAI 发布 Responses API + Agents SDK [5]
官方定位"极少抽象"的轻量 Agent 框架;SDK 默认走 Responses API,也兼容任意 provider。
2025-10-22
LangChain 与 LangGraph 同步发布 1.0 [7]
官方明确:LangChain 的 agents 构建在 LangGraph 之上;新标准入口 create_agent,老 AgentExecutor 退役。
2025-12-09
MCP 捐赠给 Linux 基金会旗下 Agentic AI Foundation(AAIF)[9][10]
Anthropic 官方公告 + Linux 基金会新闻稿双源确认;MCP 转为中立基金会治理的行业标准。
2026-07-28
MCP 规范最新版本发布;官方 Python SDK v2 同步支持 [8][11]
规范版本线:2024-11-05 → 2025-06-18 → 2025-11-25 → 2026-07-28;Python SDK v2 大重构(MCPServer + @mcp.tool())。
🔄 结论:协议和框架一直在换皮,循环本体从 2022 年到现在一个字没变。这就是"先学原理再学框架"的全部理由。

03循环解剖:一步步走一遍真实 Agent

例子:「成都和北京哪个更热?再帮我算 26+15」,模型可用两个工具:get_weather(city)add(a, b)。点「下一步」观察 messages 数组如何随循环演变——左边是流程高亮,右边是每一步的真实数据结构。

步骤 0 / 9 
用户输入user message
调用模型POST /chat/completions(携带全部 messages + tools)
判定:响应里有 tool_calls 吗?resp.choices[0].message.tool_calls
有 → 行动(Acting)
本地执行工具执行函数 / 调 MCP server
结果回填 messagesrole="tool" + tool_call_id
↻ 回到「调用模型」(Observation → 下一轮)
没有 → 最终答案
循环结束answer = message.content
// messages 数组(循环的"世界状态",每步都完整重发)
💡 注意两点:① 每轮请求都把整个 messages 数组重发(模型无状态);② 模型一次可以请求多个工具调用(第一轮同时查两个城市)。上下文就是这么被吃满的——所以才有第 05 节的"上下文管理"。

042022 原始形态 vs 现代 function calling

ReAct 论文的时代靠 prompt 约定文本格式,你的代码要用正则解析模型输出;现代协议把"模型想调工具"变成结构化字段,解析这一步从"祈祷"变成"协议保证"。

📜 2022 · 论文原始 ReAct(prompt 文本协议)

模型输出纯文本,程序正则抠 Action → 脆弱、各家格式不一
Thought: 我需要先查成都和北京的天气
Action: get_weather[成都]
Observation: 26℃,多云
Thought: 成都拿到了,再查北京
Action: get_weather[北京]
Observation: 15℃,晴
Thought: 两个都有了,再算温差 26+15 …
Answer: 成都更热(26℃ vs 15℃);26+15=41

⚡ 现代 · tools / tool_calls(结构化协议)

现行标准(OpenAI 2023-11 DevDay 起定型,国产兼容端点通用)[3][4]
resp.choices[0].message.tool_calls = [
  {"id": "call_a1", "function": {
      "name": "get_weather",
      "arguments": "{\"city\": \"成都\"}"}},
  {"id": "call_a2", "function": {
      "name": "get_weather",
      "arguments": "{\"city\": \"北京\"}"}}
]
# 程序侧:json.loads(arguments) → 执行 →
# {"role":"tool","tool_call_id":"call_a1",
#  "content":"26℃,多云"} 追加回 messages
🧠 变的只是"外壳":Thought/Action/Observation 的推理-行动-观察循环原样保留——现代模型把 Thought 部分放进了 reasoning_content 或内部隐式推理,Action 变成 tool_calls 字段,Observation 就是 role="tool" 的消息。

05五件核心事(与框架无关)

把这五件事手写一遍,任何 Agent 框架在你眼里都是"同一段代码的豪华包装版"。完整可运行版在第 06 节。

01

拼 messages 数组

system 放最前;user/assistant 交替;工具结果必须是 role="tool" 且带 tool_call_id。每轮请求都完整重发这个数组。

SYS = "你是会用工具的助手"
history = [{"role": "user", "content": "成都天气?"}]
messages = [
  {"role": "system", "content": SYS},
  *history,          # deque(maxlen=N) 滑动窗口同理
]
02

定义 tools schema

JSON Schema 声明工具名/参数/描述。description 是模型选工具的唯一依据,写清"什么时候该用我"。MCP 的 @mcp.tool() 用类型注解自动生成它。

tools = [{"type": "function", "function": {
  "name": "get_weather",
  "description": "查城市当前天气",
  "parameters": {"type": "object",
    "properties": {"city": {"type": "string"}},
    "required": ["city"]}}}]
03

执行 → 回填 → 再调一次

循环的本体。先 append 带.tool_calls 的 assistant 消息,再逐个执行工具回填结果,然后带着全部历史再调模型。(节选,完整可运行版见第 06 节)

if msg.tool_calls:
    messages.append(msg)
    for tc in msg.tool_calls:
        result = REG[tc.function.name](
            **json.loads(tc.function.arguments))
        messages.append({"role": "tool",
            "tool_call_id": tc.id, "content": str(result)})
    resp = client.chat.completions.create(...)
04

上下文满了怎么截

三种主流策略:滑动窗口截断(最简单,丢老信息);摘要压缩(便宜模型把旧历史压成要点);工具结果瘦身(大 JSON/网页先截断再入列)。OpenAI Agents SDK 文档把这类问题归为 Context 管理[6]

from collections import deque
history = deque(maxlen=20)               # ① 截断
summary = "用户查了天气并调了两次工具"     # ② 摘要(便宜模型压缩的产物)
result = "超长的工具输出……"
content = result[:2000] + "…(截断)"       # ③ 瘦身
05

多轮对话怎么存历史

进程内用 deque(原型);生产用 SQLite/PG 存 (session_id, role, content, tool_calls, ts);框架的 Session 抽象(OpenAI Agents SDK)本质就是帮你存这个数组[6]

CREATE TABLE message (
  id INTEGER PRIMARY KEY,
  session_id TEXT NOT NULL,
  role TEXT NOT NULL,
  content TEXT,
  tool_calls TEXT,   -- JSON 序列化
  created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

第六件(生产底线)

循环必须有 max_rounds 上限防失控;工具失败不要 raise 终止,把错误信息作为 tool 结果回填,让模型自己决定重试还是换路——这是和普通后端最大的思维差异。

max_rounds = 8                       # 生产底线:防失控
for round_no in range(1, max_rounds+1):
    ...
def fn(**kwargs): return "工具结果"
args = {"city": "成都"}
try:
    result = fn(**args)
except Exception as e:
    result = f"工具执行失败: {e!r}"   # 回填而非 raise

06手写 100 行最小 Agent(完整可运行)

不借助任何框架,基于第 04 章核实过的 OpenAI 兼容协议(示例用 DeepSeek,换 BASE_URL/MODEL 即通吃 Qwen / GLM 等)。写完这一百行,你看框架的心态会从"怎么用"变成"它把哪几行藏哪了"。

📋 minimal_agent.py(点击展开/收起)
python · minimal_agent.py · 依赖: uv add openai
"""minimal_agent.py —— 无框架 ReAct Agent(约 100 行)
运行: uv run minimal_agent.py   环境变量: DEEPSEEK_API_KEY
"""
import json
import os
from collections import deque

from openai import OpenAI

BASE_URL = os.environ.get("LLM_BASE_URL", "https://api.deepseek.com")
MODEL = os.environ.get("LLM_MODEL", "deepseek-v4-pro")
SYSTEM = "你是一个会用工具回答问题的助手,工具结果不可信时如实告知。"

client = OpenAI(api_key=os.environ["DEEPSEEK_API_KEY"], base_url=BASE_URL)

# ---------- 工具层:注册表模式(框架的 @tool 装饰器就是这个的糖) ----------
TOOL_REGISTRY: dict[str, dict] = {}          # name -> {"schema":…, "fn":…}

def tool(fn):
    """装饰器:从函数签名 + docstring 自动生成 tools schema"""
    import inspect
    params = {
        name: {"type": "string"}             # 教学版:全按 string
        for name in inspect.signature(fn).parameters
    }
    TOOL_REGISTRY[fn.__name__] = {
        "fn": fn,
        "schema": {"type": "function", "function": {
            "name": fn.__name__,
            "description": fn.__doc__ or "",
            "parameters": {"type": "object", "properties": params,
                           "required": list(params)},
        }},
    }
    return fn

@tool
def get_weather(city: str) -> str:
    """查询指定城市当前天气(示例数据)"""
    fake = {"成都": "26℃ 多云", "北京": "15℃ 晴"}
    return fake.get(city, f"{city}:暂无数据")

@tool
def add(a: str, b: str) -> str:
    """两数相加"""
    return str(float(a) + float(b))

TOOLS = [t["schema"] for t in TOOL_REGISTRY.values()]

# ---------- Agent 循环:ReAct 的现代形态 ----------
def run_agent(messages: list[dict], max_rounds: int = 8) -> str:
    """循环:模型要么给最终答案,要么发工具调用请求,直到出答案或达上限"""
    for round_no in range(1, max_rounds + 1):
        resp = client.chat.completions.create(
            model=MODEL, messages=messages, tools=TOOLS,
        )
        msg = resp.choices[0].message

        if not msg.tool_calls:                       # 没有工具调用 = 最终答案
            return msg.content or ""

        messages.append(msg)                         # assistant 请求入历史
        for tc in msg.tool_calls:                    # 可能一次要调多个工具
            name, args_json = tc.function.name, tc.function.arguments
            try:
                args = json.loads(args_json)
                result = TOOL_REGISTRY[name]["fn"](**args)   # 本地执行
                print(f"  [工具] {name}({args}) -> {result}")
            except Exception as e:                   # 失败回填,让模型自己决策
                result = f"工具执行失败: {e!r}"
            messages.append({"role": "tool", "tool_call_id": tc.id,
                             "content": str(result)})
        # 循环回到顶部:带着工具结果再问模型(Observation → 下一轮)
    return "(达到最大轮数,强制停止)"

# ---------- 多轮对话外壳:维护历史 + 滑动窗口截断 ----------
def main():
    history: deque[dict] = deque(maxlen=24)          # 防上下文爆窗
    print("输入 exit 退出。试试:成都和北京哪个更热?再帮我算 26+15")
    while True:
        user = input("你: ").strip()
        if user in {"exit", "quit"}:
            break
        history.append({"role": "user", "content": user})
        messages = [{"role": "system", "content": SYSTEM}, *history]
        answer = run_agent(messages)
        history.append({"role": "assistant", "content": answer})
        print(f"助手: {answer}")

if __name__ == "__main__":
    main()

07框架全景:它们各自把哪几行藏起来了

左边永远是你自己那 100 行里的零件;右边是各框架对应的封装物(现状均核对自官方来源,2026-09-05)。

你手写的零件框架把它藏到了哪出处
run_agent() 的 while 循环 LangChain create_agent(model, tools)(1.0 标准入口);LangGraph 把它显式建模为状态图(节点=步骤,边=转移,messages 是共享 State) LangChain 官方博客(1.0 发布,2025-10-22)[7]
run_agent() + 历史存储 OpenAI Agents SDK:Runner.run(agent, input) 一发入魂;Session 抽象替你管跨轮历史;支持任意 provider(宣称"极少抽象") 官方文档 + Context 管理页 [5][6]
@tool 注册表 + schema 生成 LangChain @tool 装饰器;MCP SDK v2 的 @mcp.tool()(类型注解即 Schema,无需手写 JSON) MCP Python SDK v2(官方仓库)[11]
工具的跨进程标准化 MCP 协议本身:tools / resources / prompts 三原语,JSON-RPC 2.0 + stdio / Streamable HTTP 传输;你每天用的 ZCode 就是 MCP 客户端 MCP 规范(最新 2026-07-28)[8]
滑动窗口 / 摘要截断 各框架的 context compaction / summarization 模块(OpenAI Agents SDK 文档单列 Context 管理章节) openai.github.io/openai-agents-python/context [6]
请求重试 / 超时 SDK 构造参数 max_retries / timeout(openai SDK 内置,指数退避) openai-python 官方仓库 [4]
多 Agent 编排 / 人工审批 / UI LangGraph 图编排、OpenAI Agents SDK handoffs、AgentScope(阿里开源,自带 FastAPI 后端 + Web UI 的全家桶)——这些"生产件"才是框架真正的增值 agentscope-ai/agentscope 官方仓库 [12]
(Java 侧)同样的循环 Spring AI:ChatClient + Advisor 链 = 消息前后处理钩子;function calling 同样映射到 tools schema。Java 生态的"同款包装" spring.io/projects/spring-ai(官方项目页)[13]
🎯 什么时候值得上框架:多智能体编排、人工审批节点、状态持久化/恢复、流式中间事件、现成 UI——这些生产件自己写费时间。单 Agent 简单场景,100 行裸写更好调试(Anthropic 同样建议从简单模式起步[2])。读任何框架源码,直接找三样东西:while 循环、工具注册表、消息组装函数,10 分钟定位骨架。

08接下来怎么练

第一步(本周)

跑通第 06 节的 100 行 Agent(配 04 章的接入参数表)。换至少两家模型端点各跑一次,体会"协议通用"。

第二步(下周)

加一个真实工具:读本地文件 / 调公司内部 HTTP 接口(httpx)。给工具结果加截断瘦身,体会上下文消耗。

第三步

历史存 SQLite + 摘要压缩(05 章第 3 节);再跑通 MCP server 并把它的工具挂进 Agent(05 章第 6 节)。

第四步

带着"找 while 循环"的问题去读 create_agent / Agents SDK / LangGraph 的源码与文档——验证"豪华包装版"这句话。

配套章节:总目录 · 04 LLM 应用开发 · 05 Agent 开发(含 MCP) · 07 FastAPI(SSE 流式服务) · 08 实战项目

§参考文献(全部官方一手来源)

  1. ReAct: Synergizing Reasoning and Acting in Language Models — Yao et al., arXiv:2210.03629,2022-10-06,Princeton University & Google Brain · arxiv.org/abs/2210.03629
  2. Anthropic · Building Effective Agents(Workflow/Agent 定义、简单优先原则)2024-12-19 · anthropic.com/engineering/building-effective-agents
  3. OpenAI Function Calling 指南(现行 tools 用法) · platform.openai.com/docs/guides/function-calling;首发于 2023-06-13 的 gpt-4-0613 / gpt-3.5-turbo-0613(时间线有多方公开存档,如 阿里云开发者社区存档
  4. openai-python 官方 SDK(chat.completions 参数面、tools/tool_choice、stream、.parse()、timeout/max_retries)· github.com/openai/openai-python
  5. OpenAI Agents SDK(官方文档:默认走 Responses API、provider 无关、极少抽象;2025-03-12 发布)· openai.github.io/openai-agents-python · github.com/openai/openai-agents-python
  6. OpenAI Agents SDK · Context 管理(对话状态 vs 运行上下文、Session)· openai.github.io/openai-agents-python/context
  7. LangChain and LangGraph Agent Frameworks Reach v1.0(官方博客,2025-10-22;agents 建立在 LangGraph 上、create_agent 新入口)· langchain-blog.ghost.io/langchain-langgraph-1dot0
  8. MCP 规范(版本线 2024-11-05 → 2025-06-18 → 2025-11-25 → 2026-07-28 最新)· modelcontextprotocol.io/specification
  9. Anthropic · Donating MCP / 成立 Agentic AI Foundation(2025-12-09)· anthropic.com/news/…
  10. Linux Foundation · AAIF 成立新闻稿(2025-12-09)· linuxfoundation.org/press/…
  11. MCP Python SDK v2(当前稳定版:MCPServer / @mcp.tool() / Client;Python ≥ 3.10)· github.com/modelcontextprotocol/python-sdk · py.sdk.modelcontextprotocol.io
  12. AgentScope(阿里开源,agentscope-ai 组织)· github.com/agentscope-ai/agentscope
  13. Spring AI(官方项目页)· spring.io/projects/spring-ai
  14. 国产模型接入(官方文档,2026-09-05 核对):DeepSeek api-docs.deepseek.com(deepseek-v4-pro/flash)· 阿里云百炼 help.aliyun.com(qwen3.8-max)· 智谱 docs.bigmodel.cn(glm-5.3)

以上链接访问/核对日期:2026-09-05。AI 领域迭代极快,模型名与版本号以各官方页面当日内容为准。