尧图精选

LangChain实战:从0到1构建可靠AI Agent的核心要点

🕒 发布时间:2026/9/8 6:24:55 📁 来源:尧图网络
最近被问得最多的一个问题就是“LangChain快速入门到底怎么搞”。很多人其实已经看过一遍官方文档也复制过几段示例代码但一到自己写AI Agent就发现跑通一个demo和让Agent真正干活完全是两码事。对着文档抄出来的Agent要么只会回“我无法访问外部工具”要么反复调用同一个工具就是不给你最终答案还有的聊上三轮就开始把聊天记录里的旧内容当事实逻辑全乱。这篇文章我不想做那种“一句话介绍LangChain”的科普而是直接把从0到1搭一个能实际干活的AI Agent这条路走一遍重点放在工具调用、状态管理和错误恢复这三件事上。适合刚打算入门LangChain的人也适合已经跑过几个demo、但是被工程化问题卡住的人。建议按章节顺序看每段都有能直接抄走的思路和代码。1. 动手之前先想清楚LangChain到底帮你省了什么1.1 没有LangChain的时候你写Agent要面对什么很多人对LangChain的第一印象是“一个LLM封装库”这个理解不能算错但容易把格局看小了。LLM应用开发真正的复杂度不在“调用模型”那一步而在把模型接到真实业务逻辑里之后所有边界问题都会冒出来。举一个最简单的场景你想做一个能查订单状态的助手。用户说“帮我看看A1001到哪了”你第一反应可能是直接调API拿数据再拼进Prompt让模型回答。但真正的Agent不是这种一次性的“取数-回答”流程它是一个反复决策的循环模型先判断“用户要查订单”接着决定“应该调用get_order_status这个工具”然后你的代码执行工具把结果作为新的上下文丢回给模型模型再判断“信息够不够够了就给出最终回复”。问题马上来了谁来解析模型的输出里那一段JSON格式的工具调用指令谁来维护多轮对话里已经产生的中途消息如果模型调用工具时少传了参数你是重试一次还是直接放弃如果工具连续调用了八次还在循环怎么强制停下来没有框架的话这些逻辑都要自己手写而且很容易在某个角落里漏掉一种边界情况。我经常用“带实习生”来类比这件事。你交给实习生一个任务他不光要干活还会在干活过程中产生一堆疑问、会出错、会拿回一些没用的中间结果。你需要一直盯着他告诉他下一步做什么、他做完之后把结果汇报给你、你判断对不对、不对就让他重做。LLM应用里的模型就是这个实习生LangChain等工具就是帮你把“盯人”这个过程结构化、可维护化的脚手架。1.2 LangChain的核心抽象模型层、工具层、记忆层、编排层LangChain真正有长期价值的不是某个具体类而是它提供的一组抽象。我按自己的理解把它们分成四层抽象层解决什么问题核心组件模型输入输出层把Prompt构造、模型调用、输出解析标准化ChatPromptTemplate、ChatOpenAI/ChatOllama、StrOutputParser工具层把普通Python函数包装成模型能理解、能调用的“说明书”tool装饰器、args_schema记忆层管理多轮对话历史避免无脑拼接trim_messages、LangGraph Checkpointer编排层控制Agent的决策循环、分支、恢复LangGraph StateGraph、create_react_agent这四层不是LangChain发明的概念而是LLM应用天然需要的能力。LangChain做的是把每个环节都给你一个通用的接口让你换模型、换工具、换记忆策略的时候不用把整个应用推倒重来。但这里有一个特别容易踩的认知误区框架不能消除模型的“不可靠”。LangChain能帮你规范地组织Prompt、解析输出、管理状态但模型该幻觉还是幻觉、该传错参数还是传错参数。指望“用了LangChain Agent就稳定了”的人基本都会失望。框架只是给你一套更顺手的问题排查工具真正让Agent变“能干”的是你在工具设计、流程约束、错误恢复上做的功夫。1.3 不是所有项目都需要LangChain这话放前面说清楚免得你对框架抱有不切实际的期望。如果你的需求就是“调用一次模型让它根据固定Prompt输出一段文本”那不需要LangChain直接调SDK反而更轻。我见过不少项目为了用LangChain而用LangChain结果把简单流程拆成一堆组件排查问题反而更费劲。LangChain的舒适区是需要多步决策、多个工具、多轮状态管理的场景。你要做问答机器人、代码生成助手、支持联网搜资料再总结的Agent、或者企业内部知识库助手这类“模型工具状态”的组合型应用才是它发挥价值的地方。2. 搭好第一个能“听指挥”的Agent骨架2.1 环境准备与版本选型别被老教程带偏再强调一次LangChain的版本迭代非常快网上大量教程还停留在0.0.x时代里面的接口和最新版本已经对不上了。2024年到2025年之间官方把很多核心功能从LangChain迁到了LangGraphAgent的执行层主要走LangGraph因此“只装langchain不够还要装langgraph”逐渐成了标配。我先给一个我自己比较常用的环境配置新项目直接抄mkdir my-agent-demo cd my-agent-demo python3 -m venv venv source venv/bin/activate pip install langchain0.3,0.4 \ langchain-core0.3,0.4 \ langchain-openai0.3,0.4 \ langgraph0.4 \ langchain-ollama0.3如果你用的是OpenAI兼容接口langchain-openai里的ChatOpenAI也可以直接对接兼容服务不需要额外改代码。想本地调试的话langchain-ollama是个很好的选择配合Ollama跑一个支持工具调用的小模型在开发阶段能省不少事。提示项目里一定要锁版本尤其是LangChain生态这种大版本之间接口不兼容的库。我建议在requirements.txt里写上具体版本号至少也要限制大版本范围否则过三个月重新装环境代码跑不起来非常常见。2.2 从一条最简单的Chain开始在碰Agent之前先把最基础的一条链跑通。这一步的意义是确认模型连接、Prompt构造、输出解析这几个环节都正常工作后面出问题就知道不是底层连接挂了。from langchain_core.prompts import ChatPromptTemplate from langchain_openai import ChatOpenAI prompt ChatPromptTemplate.from_messages([ (system, 你是一个严谨的技术助手回答要简洁、准确。), (human, {question}) ]) model ChatOpenAI(modelgpt-4o-mini, temperature0) chain prompt | model answer chain.invoke({question: 用一句话解释LangChain是什么}) print(answer.content)这段代码里最有LangChain味的是prompt | model这个管道操作。它把“构造Prompt”和“调用模型”拼成了一条流水线输入一个包含question的字典先被prompt格式化成消息列表再交给model调用。输出是一个AIMessage对象所以打印的时候要取.content属性。关于模型选择我建议开发阶段用一个小模型gpt-4o-mini或者本地跑一个7B级别的小参数模型都行。小模型虽然推理能力弱一些但胜在响应快、成本低适合跑通流程。等到要验证高阶agent能力时再换大模型免得每次调试都要等半天、烧一堆token。2.3 给模型配上第一个工具Agent的骨架就出来了接下来进入正题让模型学会调用工具。我拿“订单状态查询”举例因为这个场景逻辑简单一个工具函数、一个假数据库就能说明问题。from langchain_core.tools import tool tool def get_order_status(order_id: str) - str: 根据订单ID查询订单当前状态返回物流信息文本。 Args: order_id: 订单编号例如A1001。 mock_db { A1001: 已发货预计明天送达, A1002: 正在拣货预计后天发货, } return mock_db.get(order_id, 未找到该订单请确认订单号是否正确)这个函数本身没有任何“AI”成分但装饰器tool会把它包装成一个模型能识别的工具对象。模型并不直接运行这个函数它只是看到了这个工具的名称、描述、参数说明然后决定“我要调用它”并生成一段包含参数值的调用指令。最终真正执行这个函数的是你的代码不是模型。使用LangGraph prebuilt的create_react_agent来组装Agentfrom langgraph.prebuilt import create_react_agent agent create_react_agent(modelmodel, tools[get_order_status]) result agent.invoke({ messages: [{role: user, content: 帮我查一下订单A1001现在到哪了}] }) for message in result[messages]: print(message.type, :, message.content) if hasattr(message, tool_calls) and message.tool_calls: print( 调用了工具:, message.tool_calls)运行后你会在输出里看到一条很有意思的过程模型先返回一个带有tool_calls的AIMessage然后系统执行get_order_status再把工具返回结果以ToolMessage的形式追加到消息列表最后模型基于这个结果生成最终回复。这个“模型决策→执行工具→反馈结果→再决策”的循环就是AI Agent的核心运行逻辑。我在这一步想特别强调一个观点第一步不要一上来就设计一堆工具、搞复杂的LangGraph图结构。先让一个Agent只带一个工具、只解决一个问题然后把过程中的messages一个不落地打印出来亲眼看看模型是怎么做决策的。我看过太多人一上来就搭了六个工具三条分支结果模型调用哪个工具都犹豫最终效果一塌糊涂。地基只打了一个点的时候你反而能看清这个点的问题等复杂度上来再调试就难了。2.4 从Chain到Agent中间发生了什么把2.2和2.3对比一下你会发现本质区别不是“多了一个函数”而是执行路径变了。Chain是单向管道输入直接经过Prompt、模型、输出解析一步到位。Agent是循环式决策模型可能需要和工具“来回对话”好几次才能给出最终答案。这个循环就是Agent能力的来源也是风险来源——它意味着中间任何一步都可能出错而且错误会在后续循环中被放大。不过你不一定每次都要自己写这个循环。“create_react_agent”就是LangGraph premade的ReAct实现它帮你把“模型→工具→再模型”这个循环封装好了。你先用它跑通第一个Agent等你对运行逻辑有感觉了再去用LangGraph自定义更复杂的状态流。3. 让Agent真正“干活”工具调用与记忆设计的核心细节3.1 工具不是“注册函数”而是给模型写说明书我见过特别多Agent不好用问题根本不是模型不行而是工具定义写得太糊。模型看不到你的函数内部实现它只能看到函数名、函数描述、参数名、参数类型和参数描述。这四样东西决定了它能不能在正确的时候、用正确的参数调用你的工具。举个例子同样是查天气的工具tool def get_weather(city: str) - str: 获取天气。 ...模型看到这个工具只知道“传一个城市名进去”但这个city是中文名还是英文名是“北京”还是“Beijing”温度单位是什么城区天气还是整座城市天气它全靠猜。稍微复杂一点的任务猜错的概率就会很高。更好的定义from pydantic import BaseModel, Field class WeatherInput(BaseModel): city: str Field(description城市中文名例如北京、上海、广州) units: str Field( defaultcelsius, description温度单位取值为 celsius 或 fahrenheit ) tool(args_schemaWeatherInput) def get_weather(city: str, units: str celsius) - str: 查询指定城市的当前天气返回天气状况、气温和风力。 Args: city: 城市中文名。 units: 温度单位。 # 这里接真实天气API return f{city}当前晴25摄氏度风力3级关键点是描述里要把“正确用法”和“常见取值”写清楚。你可以在描述里写“例如北京、上海”帮模型建立正确的参数空间。pydantic模型里的Field description也很有用它最终会成为模型看到的部分。可以说工具定义的质量直接决定了Agent的“手”准不准。3.2 用LangGraph把Agent当成状态机来看create_react_agent虽然方便但它是一个固定结构的Agent你只能配置模型和工具。如果你想自己控制“什么情况下必须调用工具”“工具失败后怎么恢复”“到达什么条件就强制结束”就需要理解LangGraph。LangGraph和LangChain的关系可以简单这么看LangChain提供各种能力组件LangGraph提供把这些组件串成“图状流程”的编排能力。Chat模型、工具、Memory这些是零件LangGraph用来画“流水线图纸”并执行它。Graph这个词听起来唬人核心只有三个概念State全局数据通常是消息列表或其他自定义字段每个节点都能读和写。Node一个处理步骤接收State返回State的更新。Edge节点之间的连接可以是有条件的例如“判断模型是否调用了工具如果调用了就去工具节点否则结束”。下面是一个理解用的极简框架from typing import Literal, TypedDict from langgraph.graph import StateGraph, END class AgentState(TypedDict): messages: list def call_model(state: AgentState) - AgentState: response model.invoke(state[messages]) return {messages: [response]} def call_tool(state: AgentState) - AgentState: last_message state[messages][-1] # 这里要解析tool_calls并执行工具简化处理省略 return {messages: [tool_result_message]} def should_continue(state: AgentState) - Literal[tools, __end__]: last_message state[messages][-1] if hasattr(last_message, tool_calls) and last_message.tool_calls: return tools return __end__ graph StateGraph(AgentState) graph.add_node(agent, call_model) graph.add_node(tools, call_tool) graph.set_entry_point(agent) graph.add_conditional_edges( agent, should_continue, {tools: tools, __end__: END} ) graph.add_edge(tools, agent) app graph.compile()这个例子仅供参考理解生产环境我建议直接用LangGraph prebuilt里的ToolNode不要自己解析tool_calls容易在边界条件上翻车。但你要通过这个骨架理解一个核心要点Agent的每一步决策都发生在节点与节点的转移之间而图结构本身允许你插入各种“关卡”——比如统计工具调用次数、超过阈值就跳到兜底节点或者某个工具抛异常后走重试分支。LangGraph和LangChain的“区别”本质上就是编排粒度的区别。LangChain关心“怎么把Prompt和模型拼起来”LangGraph关心“这个流程什么时候循环、什么时候分支、什么时候终止”。用LangGraph写Agent与其说是写代码不如说是在设计一个业务状态机。3.3 记忆设计别把整个对话历史都塞给模型Agent的“记忆”在技术上没有魔法它就是把历史消息作为上下文传给模型。于是很多人图省事直接把所有聊天记录一股脑全塞进去。短期对话还行一旦聊了二三十轮问题就全来了上下文变长导致响应变慢、费用变高、而且模型容易被太久远又无关紧要的细节带偏。我通常把记忆策略分成三层按需组合第一层短期滑动窗口。只保留最近N轮消息比如最近6到10轮。这是性价比最高的策略大多数应用场景根本不需要更早的记忆。from langchain_core.messages import trim_messages trimmer trim_messages( max_tokens2000, strategylast, token_countermodel, include_systemTrue, ) trimmed_messages trimmer.invoke(current_messages)第二层对话摘要。当对话超过窗口上限时把早期的消息用模型生成一段摘要用摘要替换掉那些原始消息。这样用户上一周聊过的偏好信息还在但不会占太多上下文空间。第三层向量检索。类似RAG的思路把历史消息按语义切片、向量化存储。用户提到“上次我让你帮我整理的Python学习计划”系统先去向量库里召回最相关的几条历史记录再拼进上下文。这一层适合知识库类、长期陪伴类产品对个人小项目来说可以先不做成本收益不一定划算。如果你用的是LangGraph记忆会变得更简单。LangGraph的Checkpointer机制可以直接把对话状态持久化到内存或数据库里通过thread_id区分不同会话from langgraph.checkpoint.memory import MemorySaver checkpointer MemorySaver() app graph.compile(checkpointercheckpointer) config {configurable: {thread_id: user_session_001}} # 下次再传相同的thread_idAgent能恢复之前的会话状态 result app.invoke({messages: [...]}, configconfig)这个方案比手动在外部拼历史消息要干净得多也是我推荐你尽早习惯的写法。因为你在业务里区分“哪个用户在哪个会话”是一种刚需与其自己在外面维护全局状态不如把这个复杂度交给LangGraph的状态层。4. 从Demo到能上线的Agent最难的是这五个问题4.1 工具调用结果不稳定先学会把中间过程打出来很多人在Agent接上工具的初期都会遇到一个很迷的现象模型明明在一次消息里产生了tool_calls但工具执行结果返回后模型下一轮却像失忆一样说“抱歉我无法获取订单信息”。这种问题十有八九是消息结构不对工具结果没有以ToolMessage的形式正确插入消息列表。排查这类问题第一步永远是“把中间过程打出来”。不要让Agent变成一个黑盒你必须看到每一步模型到底收到了什么、产出了什么for msg in result[messages]: print(f[{msg.type}] content{msg.content}) if hasattr(msg, tool_calls) and msg.tool_calls: for call in msg.tool_calls: print( tool:, call[name], args:, call[args])看到的过程越多你对模型行为的直觉就越准。说实话做过三个以上Agent项目之后我养成了一个习惯每次调试都先看messages轨迹而不是猜“是不是Prompt写错了”。大多数“Agent不稳定”问题最终都能在这条轨迹里找到线索。4.2 上下文污染工具返回什么模型就会信什么另一个高频坑是工具返回内容过多、太杂。模型不会主动区分“这段信息是权威数据那段信息是界面噪音”你给什么它就可能用什么。比如一个天气工具返回了整段HTML页面模型可能把“页面底部版权信息里的城市名字”当成你要的答案。解决思路就八个字工具返回尽量干净。能返回结构化JSON就不要塞一长段散文能只返回三个字段就不要返回三十个字段。我在写工具函数时最后一步往往是一个精简字典的序列化把跟用户问题无关的中间字段全部丢掉。还有一点如果工具内容特别长可以先让模型或规则做一次摘要只把关键结果放回上下文。4.3 无限循环调用工具必须加“强制刹车”这是Agent最容易让人破防的问题没有之一。模型调用工具工具返回一个让它不满意的结果它再调用一次同样的工具反复循环直到把重试次数跑光。你盯着日志看到二十条一模一样的“get_order_status”调用血压很难不升高。给定两个非常实用的刹车一是给执行器设置最大迭代数。LangGraph里可以在invoke时传recursion_limittry: result agent.invoke(input, config{recursion_limit: 15}) except GraphRecursionError: print(Agent递归太深已强制终止)二是从源头约束模型。在系统Prompt里直接写一条策略如果同一工具连续两次返回相同或类似的结果就不要再重复调用直接基于现有信息向用户说明无法解决。虽然这不能保证模型100%遵守但配合轮数限制能让大多数循环在早期就停下来。我个人的经验是单纯靠Prompt限制效果一般必须在执行层也有硬性兜底。就像你不能只靠“自觉”来约束一个实习生管理层级和自动刹车机制是必须的。4.4 API超时、限流、单点故障Agent必须能优雅失败生产环境里模型API不可能永远稳定。超时、限流、网络抖动都不可避免。Agent和普通脚本不一样的是它可能在一个决策循环里调用多次模型任何一次失败都可能让整个任务失败。所以一定要给“模型调用失败”设计兜底。LangChain原生支持降级模型from langchain_openai import ChatOpenAI from langchain_ollama import ChatOllama primary_model ChatOpenAI( modelgpt-4o-mini, timeout30, max_retries1 ) fallback_model primary_model.with_fallbacks([ ChatOllama(modelqwen2.5:7b) ])这段代码的含义是主模型调用失败或超时后自动切换到一个本地模型。很多团队会在关键Agent服务里配一个本地模型做降级优先保证系统“还能动”而不是直接给用户报错。当然降级模型的回答质量可能明显下降但你可以在返回头里标记“当前是降级模式结果可能不精准”让用户有心理预期。另外工具层的异常也要处理。工具函数内部不该轻易把异常抛给执行器尤其是不该把原始堆栈信息直接放进上下文模型看到一堆英文报错可能会编出更离谱的回答。建议工具函数统一捕获异常返回给模型一段“可理解的错误说明”例如“订单服务暂时不可用请稍后再试”。4.5 可观测性Agent上线前先给自己留一条“后路”Agent是循环系统排查难度比普通接口高一个数量级。普通接口出错了看一个请求日志就够了。Agent出错了你得看“模型第一步决定了什么、为什么会决定这个、工具返回了什么、模型看到工具结果后又怎么想”的整条链路。所以我建议无论项目多小都要把Agent的关键运行轨迹记录下来。最笨也最有效的方式就是写JSONL日志把每一步messages、tool_calls、时间戳、thread_id、以及最终回复全部落盘。{ts: 2025-01-01T10:00:00Z, thread_id: session_001, step: agent, type: ai, content: , tool_calls: [{name: get_order_status, args: {order_id: A1001}}]} {ts: 2025-01-01T10:00:01Z, thread_id: session_001, step: tool, type: tool, name: get_order_status, content: 已发货预计明天送达}如果项目团队有预算LangSmith是官方配套的可观测平台可视化程度高很多。但对小团队和个人项目来说自建JSONL日志已经完全够用。重点不是工具而是“必须要有”。等出问题那天你就能体会到能回放的Agent和不能回放的Agent排查速度差距是十倍的。4.6 常见问题速查表照着查能省半天时间现象可能原因排查思路解决建议模型调用工具后下一轮像“失忆”工具结果消息没有正确插入上下文打印result[messages]看是否有类型为tool的消息检查执行器组装消息逻辑或改用prebuilt的ToolNode工具参数总传错工具描述太模糊模型靠猜看tool_calls里的args实际传了什么重写工具描述增加参数枚举和“例如”样例Agent反复调用同一工具不终止缺少循环制动机制查看日志里同一工具被调了几次设置recursion_limit在Prompt中约束失败后不再重试模型输出频繁不是JSON格式没有用结构化输出看原始model输出是否符合预期格式使用with_structured_output绑定pydantic模型聊天一长模型错误变多历史消息过多、信息噪声大看发给模型的最终消息有多长trim_messages裁剪窗口较旧消息用摘要压缩本地模型工具调用始终失败模型本身不支持tool calling查看模型是否拒生成tool_calls换支持工具调用的模型或走提示词型ReAct路线一个工具一段长文本把上下文塞满工具返回内容未裁剪查看ToolMessage的字符数工具只返回必要字段长文本先摘要这张表是我在实际项目中遇到频率最高的七类问题。你会发现大部分都不是LangChain接口不会用而是对“模型在循环中的行为”理解不够。每一条背后都对应一个实打实的调试经历把这些记下来后续开发会顺很多。5. 最后我的几点体会如果只能在这篇文章里留下一句话我想说LangChain和LangGraph入门最快的方式不是把所有概念都研究透彻再动手而是立刻写一个只有一个工具的极简Agent然后把每一步消息从头打印到尾亲眼看着模型怎么决策、工具怎么执行、结果怎么反馈。这个最小闭环一旦在你的直觉里建立起来后面很多抽象概念会自动变得清晰。几个我自己踩出来的建议你直接拿去用第一版本能锁就锁。LangChain生态迭代太快今天能跑的代码三个月后可能就因为接口变更报错所以项目里的requirements.txt一定要写清楚版本范围。第二永远让模型能看到“足够的证据”而不是“全部的信息”。工具结果该精简就精简历史消息该裁剪就裁剪别把上下文当作无限容量的垃圾桶。第三Agent上线前至少把“无限循环”和“模型API挂了”这两种情况演练一遍。你不一定要把系统做得极其复杂但至少要保证用户在Agent摆烂时得到的是一个友好的兜底回复而不是一个超时报错。说到底LangChain只是一个工具真正的Agent有没有用还是看你怎么定义工具、怎么设计流程、怎么处理失败。希望这篇能帮你少走一些我走过的弯路早点写出那个“真正能干活的Agent”。后面我还会再写几篇关于LangGraph自定义流程和RAG落地的实战文章有具体想看的场景也可以留言告诉我。
上一篇/下一篇内容由系统自动关联 返回资讯列表 →