ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

普通程序员有没有必要学习 Agent 开发?从零手写 ReAct Agent 到 LangGraph 实战

普通程序员有没有必要学习 Agent 开发?从零手写 ReAct Agent 到 LangGraph 实战

一、先说结论:有必要,而且窗口期就在当下

如果你是一个每天写 CRUD、调接口、改 Bug 的普通后端 / 前端 / 全栈程序员,看到「Agent」「智能体」这些词,第一反应很可能是:

“这不就是调一下大模型 API 吗?跟我有什么关系?”

但如果你仔细看 2024—2026 年的招聘需求和技术演进,会发现一个明显趋势:大模型不再只是一个聊天机器人,而是开始接管真实的工作流。能写 API 的工程师很多,但能把「大模型 + 工具 + 记忆 + 规划」组合成一个可靠系统的工程师,目前仍然稀缺。

这篇文章不空谈概念。我会先讲清楚 Agent 到底是什么、和普通 LLM 调用有什么本质区别,然后带你从零手写一个 ReAct Agent,最后再引入LangGraph做工程化实践。全程代码都可以直接复制运行。


二、Agent 到底是什么:先厘清三个容易混淆的概念

很多人分不清「LLM 调用」「RAG」和「Agent」,这会导致学习路线跑偏。

概念核心能力一句话理解
普通 LLM 调用单次问答你问一句,模型答一句,无记忆、无外部动作
RAG(检索增强生成)外挂知识库先从文档里检索相关内容,再让模型基于资料回答
Agent(智能体)感知—决策—行动循环模型自己决定「调用哪个工具、调用几次、什么时候停」

用一个简单的流程图表示 Agent 的工作方式:

是

否

用户输入任务

LLM 思考:现在需要做什么?

需要调用工具吗?

调用工具(查询/计算/搜索/写文件)

把工具结果返回给 LLM

输出最终答案

关键区别在于「循环」和「自主决策」。普通 LLM 调用是一次性的:输入→输出。而 Agent 是一个循环:模型在每一轮都重新判断「我完成了吗?没有的话下一步该做什么」,直到任务收敛。

这就是为什么 Agent 开发有门槛——它不再是把字符串喂给 API 那么简单,而是要设计控制流、工具协议、状态管理、错误兜底。


三、为什么普通程序员有必要学:三个现实理由

3.1 它正在变成「基本功」而非「加分项」

回顾几个时间节点:

  • 2023 年,会调 GPT API 就能吹一波;
  • 2024 年,RAG 成了企业知识库项目的标配;
  • 2025 年之后,Agent 开始渗透到客服、运维、数据分析、代码生成、办公自动化等真实场景。

当一个技能从「实验室概念」走向「业务系统」,对工程化的要求就会指数级上升。而工程化恰好是普通程序员的强项——你不需要成为算法研究员,只需要把已有的软件工程能力迁移过来。

3.2 岗位和薪资在向「复合型」倾斜

目前市面上的 Agent 相关岗位大致分两类:

  1. 算法侧:研究模型推理、规划算法、多智能体协作,门槛较高;
  2. 工程侧:负责 Agent 框架搭建、工具集成、状态管理、性能优化、生产部署,门槛并不比你现在的日常工作高多少。

普通程序员切入的是第二条路。一个能写出健壮 REST API、理解并发和幂等的后端工程师,学习 Agent 开发的速度通常比纯算法背景的人更快,因为 Agent 系统的本质就是一个带着不确定性的分布式工作流系统。

3.3 它会反哺你的传统技能,而不是取代

学习 Agent 开发会让你重新审视很多「老问题」:

  • 函数设计:给模型用的工具,参数描述要清晰到什么程度?
  • 状态管理:多轮循环中,上下文什么时候该裁剪?
  • 可靠性:当模型可能输出格式错误时,怎么兜底?

这些问题,本质上和你做后端时思考的「接口设计、缓存策略、异常处理」是同一套逻辑。学会了 Agent,你会发现自己对系统设计有了更立体的理解。

一句话总结:普通程序员学 Agent,不是转行,是升级。


四、动手前需要搞懂的三个核心概念

在写代码之前,先把三个概念讲透,否则后面代码会看不懂。

4.1 ReAct 模式

ReAct =Reasoning(推理)+ Acting(行动)。它是目前多数单 Agent 的底层范式,流程是:

模型输出一段「思考」,然后决定调用某个工具 → 工具执行 → 结果返回模型 → 模型继续思考 → 直到任务完成。

4.2 工具(Tool)协议

工具就是「模型可以调用的函数」。但模型不能直接调用函数,它只能输出文本。所以通常的做法是:

  • 你用 JSON Schema 描述工具的参数;
  • 模型输出一个结构化的「工具调用请求」;
  • 你的代码解析这个请求并真正执行函数;
  • 把执行结果再喂回模型。

4.3 状态(State)与消息列表

Agent 的「记忆」本质上就是一个消息列表(conversation history)。每一轮都维护 system prompt、历史 user/assistant 消息、工具调用记录等。这个状态对象的设计,直接决定了 Agent 的性能和可靠性。


五、上手最小 Agent:依赖与整体结构

进入实战。我们先用最少的依赖手写一个 ReAct Agent,理解底层原理。环境要求:

  • Python 3.10+
  • 一个 OpenAI 兼容的 API Key(OpenAI、DeepSeek、通义千问、Kimi 等都行)

安装依赖:

pipinstallopenai

我们需要三个文件(或者放在一个文件里也行):

  1. 工具定义:给模型看的 JSON Schema,以及真正执行的 Python 函数;
  2. Agent 核心循环:调用模型 → 解析工具调用 → 执行工具 → 把结果塞回上下文;
  3. 运行入口:初始化客户端、定义工具、发起任务。

为了贴近真实场景,我实现两个工具:

  • get_weather(city):查询城市天气(用随机数据模拟,方便无网络运行);
  • calculate(expression):安全计算数学表达式。

六、从零手写 ReAct Agent:完整代码

先贴出完整可运行代码,再逐段讲解。

# react_agent.pyimportjsonfromopenaiimportOpenAI# ============ 1. 工具的真实实现 ============defget_weather(city:str)->str:"""模拟天气查询。真实场景可以替换为调用天气 API。"""importrandom temps={"北京":8,"上海":12,"深圳":22,"杭州":15,"成都":14}weathers=["晴","多云","小雨","阴"]temp=temps.get(city,random.randint(0,30))weather=random.choice(weathers)returnf"{city}今天{weather},气温{temp}°C"defcalculate(expression:str)->str:"""安全地计算数学表达式。仅允许数字和基础运算符。"""importastimportoperator allowed_ops={ast.Add:operator.add,ast.Sub:operator.sub,ast.Mult:operator.mul,ast.Div:operator.truediv,ast.Pow:operator.pow,ast.USub:operator.neg,}def_eval(node):ifisinstance(node,ast.Expression):return_eval(node.body)ifisinstance(node,ast.Constant)andisinstance(node.value,(int,float)):returnnode.valueifisinstance(node,ast.BinOp)andtype(node.op)inallowed_ops:returnallowed_ops[type(node.op)](_eval(node.left),_eval(node.right))ifisinstance(node,ast.UnaryOp)andtype(node.op)inallowed_ops:returnallowed_ops[type(node.op)](_eval(node.operand))raiseValueError("不支持的表达式")try:tree=ast.parse(expression,mode="eval")result=_eval(tree)returnstr(result)exceptExceptionase:returnf"计算失败:{e}"# ============ 2. 给模型看的工具 Schema ============TOOLS=[{"type":"function","function":{"name":"get_weather","description":"查询指定城市的天气。当用户询问天气时调用。","parameters":{"type":"object","properties":{"city":{"type":"string","description":"城市名称,如:北京"}},"required":["city"],},},},{"type":"function","function":{"name":"calculate","description":"计算数学表达式。当用户需要算术计算时调用。","parameters":{"type":"object","properties":{"expression":{"type":"string","description":"数学表达式,如:(3+5)*2"}},"required":["expression"],},},},]# 工具名 -> Python 函数的映射TOOL_FUNCTIONS={"get_weather":get_weather,"calculate":calculate,}# ============ 3. Agent 核心循环 ============SYSTEM_PROMPT="""你是一个智能助手,可以调用工具来帮助用户完成任务。 规则: 1. 如果需要查询天气,调用 get_weather 工具。 2. 如果需要数学计算,调用 calculate 工具。 3. 如果不需要调用工具,直接回答用户。 4. 一次只能调用一个工具,工具结果返回后,你可以继续调用下一个工具或直接回答。 """defrun_agent(client:OpenAI,user_input:str,model:str="deepseek-chat",max_rounds:int=8):"""ReAct Agent 主循环。 返回最终回答文本。 """messages=[{"role":"system","content":SYSTEM_PROMPT},{"role":"user","content":user_input},]for_inrange(max_rounds):# 第 1 步:调用模型,允许模型返回工具调用请求response=client.chat.completions.create(model=model,messages=messages,tools=TOOLS,temperature=0.2,)msg=response.choices[0].message messages.append(msg)# assistant 消息入栈# 第 2 步:判断模型是否要求调用工具ifnotmsg.tool_calls:# 没有工具调用,说明模型认为任务完成,直接返回returnmsg.contentor"(模型没有返回内容)"# 第 3 步:执行所有工具调用(本例一次一个,但协议支持并行)fortool_callinmsg.tool_calls:tool_name=tool_call.function.name# 模型给的参数是 JSON 字符串,需要解析arguments=json.loads(tool_call.function.arguments)print(f"[Agent] 调用工具:{tool_name},参数:{arguments}")func=TOOL_FUNCTIONS.get(tool_name)iffuncisNone:result=f"错误:未知工具{tool_name}"else:try:result=func(**arguments)exceptExceptionase:result=f"工具执行出错:{e}"# 第 4 步:把工具结果以 tool 消息的形式加回上下文messages.append({"role":"tool","tool_call_id":tool_call.id,"content":result,})# 达到最大轮数仍未收敛return"已达到最大推理轮数,任务可能未完成。"# ============ 4. 运行入口 ============if__name__=="__main__":# 使用 OpenAI 兼容接口,换成你自己的 keyclient=OpenAI(api_key="sk-xxxxxxxxxxxxxxxx",# 你的 API Keybase_url="https://api.deepseek.com",# 可换成 openai/通义/kimi 的地址)# 单个简单任务print("="*40)print("任务 1:查天气")print("="*40)answer=run_agent(client,"北京明天天气怎么样?适合出门吗?")print(f"最终答案:{answer}\n")# 需要多次工具调用的任务print("="*40)print("任务 2:组合任务")print("="*40)answer=run_agent(client,"北京今天气温是 8 度,上海是 12 度,两座城市温差是多少?")print(f"最终答案:{answer}\n")# 不需要工具的任务print("="*40)print("任务 3:纯问答")print("="*40)answer=run_agent(client,"什么是 ReAct 模式?")print(f"最终答案:{answer}")

6.1 逐段解读

工具的真实实现与 Schema 是分离的。上面代码里,get_weather和calculate是给人(执行器)看的 Python 函数;TOOLS列表是给模型看的描述。模型只能通过 Schema 了解「有哪些工具、参数是什么」,然后输出一个 JSON 格式的调用请求。

对话状态由messages列表驱动。注意第 2 步里有两处容易忽略的细节:

  • 模型的assistant消息(包含 tool_calls)必须原样 append 回 messages;
  • 工具执行结果必须以role: "tool"并且带上tool_call_id的形式返回。

这两点缺了任何一个,API 都会报错或行为异常。这也是 Agent 开发里「状态管理」的核心体现。

循环有上限。max_rounds是安全阀。模型理论上可能陷入「反复调用工具、一直不给出最终答案」的状态,真实项目中必须设置上限,并对超限做降级处理。

6.2 运行效果示例

任务 1 的输出大致是这样(实际结果因模型的随机性会略有差异):

======================================== 任务 1:查天气 ======================================== [Agent] 调用工具:get_weather,参数:{'city': '北京'} 最终答案:北京今天多云,气温 8°C。8 度的气温偏低,建议穿厚外套或羽绒服出门,注意保暖。

任务 2 展示了 Agent 的「多步推理」能力:

======================================== 任务 2:组合任务 ======================================== [Agent] 调用工具:get_weather,参数:{'city': '北京'} [Agent] 调用工具:get_weather,参数:{'city': '上海'} [Agent] 调用工具:calculate,参数:{'expression': '12 - 8'} 最终答案:北京今天气温 8°C,上海今天气温 12°C,两座城市的温差是 4°C。

注意,模型自己拆解了任务:先查两地天气,再计算温差。这就是 Agent 和普通 LLM 调用的本质区别——它会自主规划执行步骤。


七、手写 Agent 的三个隐形坑

手写代码能帮你理解原理,但生产环境会遇到三个大问题,这正是我们引入框架的原因。

7.1 工具调用参数解析不可靠

模型偶尔会返回非法 JSON、漏参数、参数类型错误。上面的代码直接json.loads,遇到坏格式就会抛异常。真实场景需要 try/except + 重试 + 格式纠正。

7.2 循环没有「记忆管理」

messages.append会让上下文无限增长。一轮复杂任务的 tool_calls 可能产生几千 token 的历史,多轮下来成本飙升、还容易让模型「遗忘」早期关键信息(上下文窗口溢出)。

7.3 没有中断与分支

现实中的 Agent 不一定是「一直循环到结束」的单线流程。你可能需要在某个节点做人工审核、失败重试、并行分支。手写的 while 循环很快会变成难以维护的意大利面条。

所以下一步,我们用LangGraph把上面的逻辑「工程化」。


八、进阶实战:用 LangGraph 构建可控 Agent

LangGraph 是 LangChain 团队出的一个有状态、可编排的 Agent 框架。它的核心思想是:把 Agent 的每一步建模成图(Graph)里的节点,状态在节点之间传递。

先安装依赖:

pipinstalllanggraph langchain-openai langchain-core

下面是功能与手写版基本一致的 LangGraph 实现:

# langgraph_agent.pyimportjsonfromtypingimportAnnotated,TypedDictfromlangchain_openaiimportChatOpenAIfromlangchain_core.toolsimporttoolfromlanggraph.graphimportStateGraph,START,ENDfromlanggraph.graph.messageimportadd_messages# ============ 1. 定义状态:本质是消息列表 ============classAgentState(TypedDict):# add_messages 会把新消息追加到列表里,而不是覆盖messages:Annotated[list,add_messages]# ============ 2. 用装饰器定义工具(LangChain 会自动生成 Schema)============@tooldefget_weather(city:str)->str:"""查询指定城市的天气。当用户询问天气时调用。"""importrandom temps={"北京":8,"上海":12,"深圳":22,"杭州":15,"成都":14}weathers=["晴","多云","小雨","阴"]temp=temps.get(city,random.randint(0,30))weather=random.choice(weathers)returnf"{city}今天{weather},气温{temp}°C"@tooldefcalculate(expression:str)->str
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进