写了这么久 Agent我发现一个怪现象很多人能把 Agent 跑起来却说不清它到底是什么。问他 agent 由什么组成他脱口而出model tools再追问工具结果怎么回到模型里他就卡壳了。其实 Agent 没那么玄它的全部秘密就是一句话——agent 模型 工具 一个是否继续的循环。LangChain 的 create_agent 做的事不过是把这个循环标准化、可配置化。今天这篇文章我们就把这个循环拆开看清楚每一个齿轮也看看那些自己手搓循环、最后线上跑飞的惨案。第一节先别急着写代码把Agent这个词拆开Agent 这个词被吹得太玄好像不整点智能体自主决策就对不起这个称呼。但扒掉外衣它就是三块东西拼起来的。模型是大脑但只负责想。它接收一个 messages 列表吐出文本或者 tool_calls自己不碰任何外部系统。工具是手脚负责做。一个用 tool 装饰的函数查数据库、调第三方 API、做计算把现实世界的数据拉进来。循环是灵魂负责不断想。模型这次说我要调 get_weather循环就把结果拿回来塞进上下文再问模型一次直到它不再要调工具。没有这个循环模型只是个一次性的问答机有了它模型才真正动了起来。create_agent 不是发明了什么新能力它只是把你本该手写的循环标准化了。# 伪代码Agent 的本质就这三样东西agent(model,tools,loop)whileloop.should_continue():outmodel(messages)# 模型推理ifout.tool_calls:messagesexecute(tools,out.tool_calls)# 工具结果回灌else:returnout# 不再要工具出最终答案第二节create_agent 帮你拼的不是魔法是五块标准件LangChain 把上面那套东西封装成一个函数。你不用自己写 while不用自己管 tool 结果怎么塞回去五块标准件填进去就行。fromlangchain.agentsimportcreate_agentfromlangchain_openaiimportChatOpenAIfromlangchain.toolsimporttoolfromlanggraph.checkpoint.memoryimportMemorySavertooldefget_weather(city:str)-str:查询指定城市的当前天气。# 实际调用天气 API ...returnf{city}今天晴25°Cagentcreate_agent(modelopenai:gpt-4o-mini,# ① 模型tools[get_weather],# ② 工具prompt你是一个天气助手用简洁中文回答。,# ③ 系统提示middleware[log_middleware],# ④ 横切中间件checkpointerMemorySaver(),# ⑤ 记忆 / 检查点)resultagent.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 内部跑的就是标准的 ReActReasoning Acting推理 → 行动 → 观察 → 再推理。推理Reason。把当前 messages 丢给模型模型输出文本或者带 tool_calls 的结构化结果。行动Act。如果模型要调工具循环取出 tool_calls逐个执行拿到原始返回。观察Observe。把工具返回值包装成 ToolMessageappend 回 messages这一步叫回灌上下文。再推理。带着新上下文重新调模型直到模型不再输出 tool_calls循环结束给出最终答案。# ReAct 循环的伪代码create_agent 内部大致如此defrun_react(model,tools,messages,max_steps10):forstepinrange(max_steps):# ① 有 step 上限保命符aimodel.invoke(messages)messages.append(ai)ifnotai.tool_calls:# ② 模型说不调了结束returnai.contentforcallinai.tool_calls:# ③ 逐个执行工具try:obscall.tool.invoke(call.args)exceptExceptionase:# ④ 错误隔离obsf工具执行失败:{e}messages.append(ToolMessage(obs,tool_call_idcall.id))return达到最大步数已停止# ⑤ 兜底退出注意这段伪代码里的几个细节max_steps 是保命符没有它循环可能永远不收try/except 是隔离墙一个工具挂了不能让整个 Agent 崩。这两行就是你和线上事故之间的距离。第四节工具调用的真面目——请求和响应的结构Agent 的智能全藏在消息流里。模型不是直接调用你的 Python 函数而是先在 AIMessage 里声明我要调谁、传什么参数循环执行完再把结果以 ToolMessage 回填。# 模型一次带 tool_calls 的输出简化AIMessage(content,tool_calls[{name:get_weather,args:{city:北京},id:call_abc123,# 用于和 ToolMessage 配对}],)# 工具执行后回填一条 ToolMessageToolMessage(content晴25°C,tool_call_idcall_abc123,# 必须和上面的 id 一致)几个关键点tool_calls 是一条数组。模型一次可以要求调多个工具循环并行或串行执行。args 是模型自己填的。它依据 tools 的 schema 生成参数填错了就会报错所以工具 schema 写得清楚很重要。id / tool_call_id 是配对键。ToolMessage 必须带上对应的 id模型才能把哪个结果对应哪次调用对上号。content 可以为空。带 tool_calls 的 AIMessage 往往 content 是空字符串真正的话留在最后一次没有 tool_calls 的输出里。理解了消息流的结构你才真正能调试 Agent——哪一步模型乱填了参数、哪条 ToolMessage 没配对上一目了然。第五节别手搓循环——我见过的几个跑飞现场道理讲完说点扎心的。下面这些坑都是真实发生过的事。反例 / 反模式自己写 while 调模型的人最常见的反例是觉得不就是循环调模型吗我自己写。于是# 反模式裸 while 手搓 Agentmessages[SystemMessage(你是助手),HumanMessage(查一下订单 12345)]whileTrue:# ① 没有 step 上限aillm.invoke(messages)messages.append(ai)ifai.tool_calls:forcallinai.tool_calls:resultcall.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。