create_agent 实战:一个 Agent 是怎么被组装出来的

写了这么久 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。

相关推荐
灵动的艺术1 小时前
如一 Agent 架构解读(六):阈值、联动与坑位——压缩系统的肌理
人工智能·agent·如一
武子康1 小时前
把调用移进同一个类之后:一次 Spring 事务回滚反例
人工智能·后端·agent
百慕大三角2 小时前
为了在 Linux CI 里跑小程序 E2E,我把微信官方基础库搬进了浏览器:wxa-runner 开源实践
微信小程序·agent·测试
德林恩宝2 小时前
搜索 Agent 的两条路——训练一个,还是工程搭一个
人工智能·大模型·llm·agent
进击的雷神3 小时前
怕 AI Agent 乱删文件?Tank-OS 把它锁进不可变系统
人工智能·ai·api·agent
FITA阿泽要努力3 小时前
/goal 命令:给 Agent 设定目标
服务器·人工智能·python·agent
Json____4 小时前
用 Node.js + Vue3 打造一个家居装修 AI 智能咨询助手
node.js·agent·网站开发·智能体·wwwoop.com
一缕82年的清风4 小时前
当成千上万个Agent涌入系统:高并发大模型网关怎么搭?
agent
张张123y9 小时前
后端第十四课:Agent 记忆系统(让 Agent 记住上下文和用户,跨会话不健忘)
java·大数据·人工智能·python·agent·记忆