从零构建 LLM Agent(一):用 LangGraph 跑通 ReAct Agent
系列说明
本系列围绕「从用到懂到造」展开,记录基于自部署 LLM 构建 Agent 的实践。
| 篇 | 主题 | 阶段 |
|---|---|---|
| 1(本文) | 用create_react_agent跑通一个 ReAct Agent | 用 |
| 2 | 手写StateGraph,拆解 ReAct 循环的状态机实现 | 懂 |
| 3 | 脱离预置图,自实现 agent loop | 造 |
| 4 | 引入长期记忆:向量检索与 RAG | 造 |
| 5 | 多 Agent 协作与人机协同(human-in-the-loop) | 造 |
| 6 | 生产化:可观测、容错与部署 | 工程 |
运行环境:Python 3.11.15 · langgraph 1.2.9 · langchain-openai 1.3.5 · langchain-core 1.4.9
模型:Qwen3-235B-A22B,公司内网自部署,暴露 OpenAI 兼容接口(/v1/chat/completions)。
适用场景:本文不依赖 OpenAI / Anthropic 官方 API,所有调用指向自部署的 Qwen3,对同样使用自部署模型(vLLM / Ollama 部署 Qwen、DeepSeek 等)的场景可直接复用。
1. Agent 与 Chatbot 的边界
从工程视角,二者的差别集中在控制流归属与是否具备外部交互能力:
| 维度 | Chatbot | Agent |
|---|---|---|
| 控制流 | 调用方驱动,单轮请求-响应 | LLM 驱动,内部循环 |
| 工具调用 | 无 | 通过 function calling 协议调用外部函数 |
| 状态 | 对话历史 | 短期消息链 + (可选)长期记忆 |
| 终止条件 | 单次生成结束 | LLM 不再发起 tool_call |
据此可给出工程定义:Agent 是以 LLM 为决策核心、通过 tool calling 协议与外部环境交互、由 LLM 自主控制循环终止的有状态系统。
其最小实现可表述为一个循环:
while True: resp = llm(messages, tools=tools) if not resp.tool_calls: break messages += execute(resp.tool_calls) # 得到 Observation messages += resp return resp.contentLangGraph 的create_react_agent即对该循环的图封装。本文先用它跑通,第 2 篇再拆开。
2. 前置验证:模型是否支持 tool calling
tool calling 是 Agent 的硬依赖:LLM 必须能按 OpenAI function calling 协议返回结构化的tool_calls,框架据此分发到对应函数。Qwen3 走 OpenAI 兼容接口,需实测确认。
验证脚本:
fromopenaiimportOpenAI client=OpenAI(base_url="http://10.138.17.22:3000/v1",api_key="sk-xxx")tools=[{"type":"function","function":{"name":"get_weather","description":"查询某城市实时天气","parameters":{"type":"object","properties":{"city":{"type":"string"}},"required":["city"]},}}]resp=client.chat.completions.create(model="Qwen3-235B-A22B",messages=[{"role":"user","content":"北京今天天气怎么样?请用工具查询"}],tools=tools,)print(resp.choices[0].message.tool_calls)# -> [ChatCompletionMessageToolCall(name='get_weather', arguments='{"city": "北京"}')]模型正确返回了结构化工具调用,未编造天气内容。该协议链路可用,后续基于langchain-openai的ChatOpenAI.bind_tools()可直接复用。
备注:Qwen3 默认开启 thinking 模式,会在
content中输出推理过程。在 tool calling 场景下其content为空、仅返回tool_calls,不影响框架解析。
3. 环境与依赖
独立 conda 环境隔离依赖:
conda create-nagentpython=3.11-yconda activate agent pipinstalllanggraph langchain-openai openai requests选型说明:
- Python 3.11:Python 3.9 已于 2025-10 EOL,LangGraph 1.x 主流支持 3.10/3.11。
- langchain-openai:因 Qwen 暴露 OpenAI 兼容接口,
ChatOpenAI可零适配接入,无需额外 Provider。 - 版本:langgraph 1.2.9 / langchain-openai 1.3.5 / langchain-core 1.4.9。框架迭代快,复现请对齐版本。
已知问题:另有 embedding 服务(bge-m3,
/v1/embeddings)当前网络不通,本篇不涉及,留待第 4 篇 RAG 部分处理。
4. 实现:三文件结构
config.py 配置集中管理(endpoint / api_key / model) tools.py 工具集合,@tool 声明 agent.py 构建 agent、运行与 trace 输出4.1 工具定义
工具是普通 Python 函数,@tool装饰器基于函数签名 + docstring自动生成 JSON Schema,作为 tool calling 协议中的function.parameters下发给 LLM。因此 docstring 既是文档也是契约,质量直接影响模型选 tool 的准确率。
fromlangchain_core.toolsimporttool@tooldefget_weather(city:str)->str:"""查询指定城市的实时天气。"""fake_db={"北京":"晴,25°C","上海":"多云,28°C",...}returnfake_db.get(city,f"暂无{city}天气数据")本篇共定义四个工具:get_weather、get_current_time、calculate、get_stock_price,均为 mock 数据。其中calculate对表达式做了字符白名单校验并以禁用__builtins__的eval执行,避免注入。
4.2 构建与运行
fromlangchain_openaiimportChatOpenAIfromlanggraph.prebuiltimportcreate_react_agent llm=ChatOpenAI(model="Qwen3-235B-A22B",base_url="...",api_key="...",temperature=0.7)agent=create_react_agent(llm,[get_weather,get_current_time,calculate,get_stock_price],prompt="你是一个个人助手,优先调用工具获取信息,不要编造。用中文回答。",)result=agent.invoke({"messages":[{"role":"user","content":"..."}]})create_react_agent内部构建的状态图:
┌──────────┐ tool_calls 非空 ┌──────────┐ │ agent │ ────────────────▶│ tools │ │ (调 LLM) │◀──────────────── │ (执行函数) │ └──────────┘ ToolMessage 回填 └──────────┘ │ tool_calls 为空 ▼ END (输出 AIMessage)状态以消息列表为载体(message-based state):agent 节点产出AIMessage(含tool_calls),tools 节点执行后产出ToolMessage(含结果),二者均 append 到messages,再回流 agent 节点,直至tool_calls为空终止。第 2 篇会用StateGraph显式重建此结构。
5. 行为验证与 trace 解读
5.1 工具链式协作(串行)
输入:「现在几点了?腾讯股票多少钱?持有 100 股市值多少港元?」
[1] user: 现在几点了?腾讯股票多少钱?持有100股市值多少港元? [2] AIMessage tool_calls: get_current_time({}) [3] ToolMessage: 2026年07月15日 16:11:59 Wednesday [4] AIMessage tool_calls: get_stock_price({'symbol': '腾讯'}) [5] ToolMessage: 腾讯控股 当前 385.6 港元,+1.2% [6] AIMessage tool_calls: calculate({'expression': '100 * 385.6'}) [7] ToolMessage: 100 * 385.6 = 38560.0 [8] AIMessage: 市值约为 38,560 港元。第 [6] 步中calculate的入参385.6来源于第 [5] 步get_stock_price的返回。LLM 在第 [4]→[5] 取得股价前无法发起计算,故 [2][4][6] 呈串行依赖。这条 trace 体现了 agent 对跨步骤数据依赖的隐式规划能力。
5.2 无依赖调用(并行)
输入:「北京和上海今天哪个更暖和?」
[1] user: 北京和上海今天哪个更暖和? [2] AIMessage tool_calls: get_weather({'city': '北京'}) tool_calls: get_weather({'city': '上海'}) # 同一 AIMessage 内并行 [3] ToolMessage: 北京 晴,25°C [4] ToolMessage: 上海 多云,28°C [5] AIMessage: 上海更暖和。两个查询无数据依赖,模型在单条AIMessage中发起多个tool_calls,框架并行执行。对比 5.1,可见串行/并行由任务的数据依赖决定,由模型在生成 tool_calls 时隐式调度。
5.3 工具边界判断
输入「介绍一下你自己」时,模型不发起任何tool_calls,直接返回AIMessage。agent 循环在首轮即终止。这说明工具是否调用由模型基于 prompt 与 tool schema 自主判断,而非强制。
6. 小结
- ReAct 的工程实现是一个以消息列表为状态、以
tool_calls是否为空为终止条件的循环; @tool基于签名与 docstring 生成 schema,是工具契约的来源;create_react_agent将上述循环封装为 agent↔tools 两节点的状态图;- agent 的自主性体现在:工具选择、跨步数据依赖规划、串并行调度、终止判断,均由 LLM 隐式完成。
7. 下一阶段
create_react_agent隐藏了状态结构与转移逻辑。下一篇改用langgraph.graph.StateGraph显式定义状态、节点与边,重建等价的 ReAct 图,拆解状态如何在节点间流转、循环如何被tool_calls条件边控制。
对应代码见 series 仓库
v1-first-agenttag。