写了这么久 Agent,我发现一个怪现象:很多人能把 Agent 跑起来,却说不清它到底是什么。问他 agent 由什么组成,他脱口而出"model + tools",再追问"工具结果怎么回到模型里",他就卡壳了。其实 Agent 没那么玄,它的全部秘密就是一句话------agent = 模型 + 工具 + 一个"是否继续"的循环。LangChain 的 create_agent 做的事,不过是把这个循环标准化、可配置化。今天这篇文章,我们就把这个循环拆开,看清楚每一个齿轮,也看看那些自己手搓循环、最后线上跑飞的惨案。

第一节:先别急着写代码,把"Agent"这个词拆开
Agent 这个词被吹得太玄,好像不整点"智能体""自主决策"就对不起这个称呼。但扒掉外衣,它就是三块东西拼起来的。
- 模型是大脑,但只负责"想"。 它接收一个 messages 列表,吐出文本或者 tool_calls,自己不碰任何外部系统。
- 工具是手脚,负责"做"。 一个用 @tool 装饰的函数,查数据库、调第三方 API、做计算,把现实世界的数据拉进来。
- 循环是灵魂,负责"不断想"。 模型这次说"我要调 get_weather",循环就把结果拿回来塞进上下文,再问模型一次,直到它不再要调工具。
没有这个循环,模型只是个一次性的问答机;有了它,模型才真正"动"了起来。create_agent 不是发明了什么新能力,它只是把你本该手写的循环标准化了。
python
# 伪代码:Agent 的本质就这三样东西
agent = (model, tools, loop)
while loop.should_continue():
out = model(messages) # 模型推理
if out.tool_calls:
messages += execute(tools, out.tool_calls) # 工具结果回灌
else:
return out # 不再要工具,出最终答案
第二节:create_agent 帮你拼的不是魔法,是五块标准件
LangChain 把上面那套东西封装成一个函数。你不用自己写 while,不用自己管 tool 结果怎么塞回去,五块标准件填进去就行。
python
from langchain.agents import create_agent
from langchain_openai import ChatOpenAI
from langchain.tools import tool
from langgraph.checkpoint.memory import MemorySaver
@tool
def get_weather(city: str) -> str:
"""查询指定城市的当前天气。"""
# 实际调用天气 API ...
return f"{city} 今天晴,25°C"
agent = create_agent(
model="openai:gpt-4o-mini", # ① 模型
tools=[get_weather], # ② 工具
prompt="你是一个天气助手,用简洁中文回答。", # ③ 系统提示
middleware=[log_middleware], # ④ 横切中间件
checkpointer=MemorySaver(), # ⑤ 记忆 / 检查点
)
result = agent.invoke(
{"messages": [("user", "北京天气怎么样?")]},
config={"configurable": {"thread_id": "u_001"}},
)
print(result["messages"][-1].content)
五个组装件各自干什么,说清楚:
- model 是决策器。 可以是字符串 "openai:gpt-4o-mini",也可以是任意 ChatModel 实例,create_agent 不关心你用哪家。
- tools 是能力清单。 一个 @tool 函数列表,每个都带 schema,模型靠 schema 知道怎么填参数。
- prompt 是人设与约束。 一段 SystemMessage,决定 Agent 的语气、边界和不能做的事。
- middleware 是横切逻辑。 日志、重试、往上下文注入额外状态,这些不该写进主流程,所以挂在 middleware 上。
- checkpointer 是记忆。 把消息和中间状态落盘,同一 thread_id 能续上对话,也能跨会话回放。
create_agent 的真正价值,是把"循环该怎么跑、工具结果怎么回灌、状态怎么存"这些脏活标准化,让你只填参数。

第三节:ReAct 循环剥掉包装,就是一段 while
很多人以为 Agent 底层多神秘,其实 create_agent 内部跑的,就是标准的 ReAct(Reasoning + Acting):推理 → 行动 → 观察 → 再推理。
- 推理(Reason)。 把当前 messages 丢给模型,模型输出文本,或者带 tool_calls 的结构化结果。
- 行动(Act)。 如果模型要调工具,循环取出 tool_calls,逐个执行,拿到原始返回。
- 观察(Observe)。 把工具返回值包装成 ToolMessage,append 回 messages,这一步叫"回灌上下文"。
- 再推理。 带着新上下文重新调模型,直到模型不再输出 tool_calls,循环结束,给出最终答案。
python
# ReAct 循环的伪代码(create_agent 内部大致如此)
def run_react(model, tools, messages, max_steps=10):
for step in range(max_steps): # ① 有 step 上限,保命符
ai = model.invoke(messages)
messages.append(ai)
if not ai.tool_calls: # ② 模型说不调了,结束
return ai.content
for call in ai.tool_calls: # ③ 逐个执行工具
try:
obs = call.tool.invoke(call.args)
except Exception as e: # ④ 错误隔离
obs = f"工具执行失败: {e}"
messages.append(ToolMessage(obs, tool_call_id=call.id))
return "达到最大步数,已停止" # ⑤ 兜底退出
注意这段伪代码里的几个细节:max_steps 是保命符 ,没有它循环可能永远不收;try/except 是隔离墙,一个工具挂了不能让整个 Agent 崩。这两行,就是你和线上事故之间的距离。

第四节:工具调用的真面目------请求和响应的结构
Agent 的"智能"全藏在消息流里。模型不是直接调用你的 Python 函数,而是先在 AIMessage 里声明"我要调谁、传什么参数",循环执行完,再把结果以 ToolMessage 回填。
python
# 模型一次带 tool_calls 的输出(简化)
AIMessage(
content="",
tool_calls=[{
"name": "get_weather",
"args": {"city": "北京"},
"id": "call_abc123", # 用于和 ToolMessage 配对
}],
)
# 工具执行后,回填一条 ToolMessage
ToolMessage(
content="晴,25°C",
tool_call_id="call_abc123", # 必须和上面的 id 一致
)
几个关键点:
- tool_calls 是一条数组。 模型一次可以要求调多个工具,循环并行或串行执行。
- args 是模型自己填的。 它依据 tools 的 schema 生成参数,填错了就会报错,所以工具 schema 写得清楚很重要。
- id / tool_call_id 是配对键。 ToolMessage 必须带上对应的 id,模型才能把"哪个结果对应哪次调用"对上号。
- content 可以为空。 带 tool_calls 的 AIMessage 往往 content 是空字符串,真正的"话"留在最后一次没有 tool_calls 的输出里。
理解了消息流的结构,你才真正能调试 Agent------哪一步模型乱填了参数、哪条 ToolMessage 没配对上,一目了然。

第五节:别手搓循环------我见过的几个跑飞现场
道理讲完,说点扎心的。下面这些坑,都是真实发生过的事。
反例 / 反模式:自己写 while 调模型的人
最常见的反例,是觉得"不就是循环调模型吗,我自己写"。于是:
python
# 反模式:裸 while 手搓 Agent
messages = [SystemMessage("你是助手"), HumanMessage("查一下订单 12345")]
while True: # ① 没有 step 上限!
ai = llm.invoke(messages)
messages.append(ai)
if ai.tool_calls:
for call in ai.tool_calls:
result = call.tool.invoke(call.args) # ② 没有错误隔离
messages.append(ToolMessage(result, call.id))
else:
break
这段代码的四个致命伤:
- 没有 max_steps。 模型一旦陷入"调工具 → 看结果 → 再调同一个工具"的死循环,进程永远不退出,token 一直烧。
- 没有错误隔离。 工具抛个异常,整个 while 直接崩,连一次降级回复都没有。
- 历史无裁剪。 每次都把完整 messages 回灌,上下文线性膨胀,跑到后面单次调用几万 token,账单爆炸。
- 没有预算护栏。 调多少次工具、花多少 token,全凭模型心情,线上出了问题你才发现钱烧光了。
我见过一个团队,第三方订单接口抖了十分钟,这种裸循环不知疲倦地疯狂重试,把工单池直接冲爆,单次会话烧掉平时一百倍的 token。
create_agent 默认就给你 max_steps、错误隔离、上下文管理和预算钩子------你手搓的 while 一个都不给你。这不是偷懒的问题,是工程底线的差距。
所以下次想自己写 Agent 循环时,先问一句:你的 max_steps 写在哪?你的 try/except 包了谁?答不上来,就用 create_agent。
