最近我在写一套内部自动化小工具的时候,又跟 Agent 打了不少交道。老实说,LangChain Agents 这个方向,网上教程多是讲概念、贴几个 demo,但真正上手把一个自动化工作流从零搭起来,中间会遇到一堆文档里不会写的问题。这篇就按我自己的实操经验,把"怎么用 LangChain Agents 把一件重复性任务变成自动化流程"这件事掰开揉碎讲清楚,包括设计思路、组件搭建、完整代码和踩坑记录,适合已经会用 LangChain 做基础调用、但还没怎么碰过 Agent 的同学。
我自己的经验是,Agent 不是炫技,它解决的是真实痛点:流程太容易变。以前我写爬虫和数据处理脚本,最烦的就是产品经理过来说"这个数不对,换个渠道抓"、"摘要格式改一下"。每一次小改动都要重新走一遍代码。后来换成 LangChain Agents 来做这件事,流程里的"决策"部分让模型来,我只要把工具和边界定义好,改动成本明显降下来了。这篇内容就是我折腾 Agent 工作流的一套完整沉淀。
1. Agent 工作流的设计思路与拆解
1.1 为什么我不用普通 Chain,而是上 Agent
很多人学 LangChain 的时候都从 Chain 开始,LLMChain、SequentialChain 用得挺顺,然后看到 Agent 就有点发怵,觉得这是个黑盒。我最早也是这么过来的。但我后来发现一个很实际的差别:Chain 的本质是一条写死的管道,你告诉程序"第一步做什么、第二步做什么、第三步做什么",每一步的输入输出都是固定的。如果业务流程稳定、输入格式统一,Chain 真的够了,完全没必要上 Agent。
但是现实里的自动化需求往往长这样:用户扔过来一个主题,我可能要先去查一点背景资料,查完了发现资料不全,还需要再补一个维度的数据,补完再决定这个摘要按什么结构写。这些"决定下一步干什么"的环节,如果用 Chain 写,你得把所有分支穷举一遍,而且数据源头一变,分支就作废。Agent 的思路不一样:它让大模型拿到任务之后,自己判断当前该调用哪个工具、拿到结果后再判断下一步做什么,直到它认为任务完成。这就是 Agent 最核心的价值:把"流程控制权"从程序员手里部分移交给了模型。
这么说可能有点抽象。打个比方——Chain 像是流水线,每个工位固定做一件事;Agent 像是一个带工具箱的工人,他只接到一个最终目标:把墙刷完。他看一眼墙面,决定先用砂纸打磨,接着上底漆,刷完发现有一块没盖住,再补一笔。这个"看一眼、想一想、动手做、再看看结果"的过程,就是 Agent 的决策循环。
1.2 一次 Agent 运行背后的决策循环
LangChain 的 Agent 本质上是基于 ReAct(Reasoning + Acting)思想实现的。所谓 ReAct,就是让模型在每一步都先输出一段"思考"(Thought),然后决定要做什么(Action),并且明确这个动作要调用的工具和参数(Action Input),得到工具返回结果后再继续思考(Observation)。这个循环会反复进行,直到模型输出了最终答案(Final Answer)。
我把一个典型循环拆出来给你看,实际运行日志里大概长这样:
> Entering new AgentExecutor chain... Thought: 我需要先收集这个主题最近的公开信息,再决定摘要结构。 Action: search_info Action Input: {"query": "xxx主题 最新进展"} Observation: 返回了三条相关内容... Thought: 内容够了,但是缺少数据表格,我需要用爬取工具补一个字段。 Action: fetch_data Action Input: {"url": "..."} Observation: 数据已获取... Thought: 现在信息齐全,我来编写摘要并保存为文件。 Final Answer: 已完成摘要,文件路径为...这个过程跟我们人处理任务的逻辑是一样的:先想,再做,看结果,再调整。LangChain 的AgentExecutor就是那个循环驱动器,它会不停地把模型最近的思考、动作、观察结果重新塞回 prompt,直到模型输出Final Answer,或者达到我们设定的最大循环次数。
理解了这一点,你就明白了为什么 Agent 工作流跟传统脚本开发完全不是一个套路。它要求你先定义好"工具边界"——Agent 手里有哪些工具可用、每个工具能干什么、不能干什么;同时你要给 Agent 一个清晰的"目标描述",剩下的是让它自己探索怎么把目标落地。坏消息是它没有普通 Chain 那么可控,好消息是它扛得住需求变化。
1.3 规划工作流时要先想清楚的四个边界
在写任何一个 Agent 自动化流程之前,我会先花半小时想清楚四个问题,比直接写代码重要得多。
第一个是目标的边界。你的工作流到底是"完成一个可以直接交付的结果",还是"把中间过程跑出来给人看"?比如我之前做的自动周报,目标定义是"根据本周数据记录,生成一份包含结论、风险点、下步建议的 Markdown 周报,并保存到指定目录"。这个目标描述里,既有内容要求,也有输出格式要求,还有动作要求,Agent 才知道怎么收敛。
第二个是工具的边界。Agent 能调用什么,不能调用什么,必须提前想好。比如一个做数据整理的 Agent,手里有数据库查询工具、表格处理工具,但你不应该给它"删除文件"这种危险能力。工具越垂直、功能越单一,Agent 就越容易正确选择。给一个大而全的工具,大概率会出现参数乱传的问题。
第三个是失败的边界。Agent 跑偏了怎么办?调用的工具报错了怎么办?很多新手只写了 happy path,一旦某个工具返回异常,Agent 可能就开始胡说八道了。我通常会给关键工具加异常返回兜底,同时在AgentExecutor里打开handle_parsing_errors,至少让流程不至于直接崩掉。
第四个是结束的边界。Agent 什么时候该停?除了模型自己判断任务完成,我们还要设置max_iterations硬上限,防止它陷入死循环烧 token。我一般根据任务复杂度设 5~10 次,复杂任务再调高一点。
这四个边界一旦清晰了,代码层面的工作其实就只是把这些决策翻译成配置和参数。
2. 核心组件搭建与关键参数解析
2.1 模型选择:不是所有模型都适合当 Agent 的大脑
Agent 对模型的依赖程度比普通 Chain 高很多,这个我必须放在前面说。模型至少要具备"理解工具描述 + 按要求输出结构化动作"的能力。如果模型能力弱,出现的情况就是:Thought 写了一大堆,Action 却不知道调哪个工具,或者 Action Input 格式奇葩,解析器直接报错。
我现在选型会给两个基本要求:
- 第一,支持工具调用(Function Calling / Tool Use)或具备很强的指令跟随能力。
- 第二,上下文窗口不能太小,起码 32k 起步,因为 Agent 的每轮循环都会把历史观察追加到 prompt 里,窗口小很容易撑爆。
具体的模型厂商、版本你得根据自己的预算和部署环境来定,我这边就不写死具体牌子了,免得变成过时信息。方向上,如果做中文场景,可以优先中文能力强的模型;如果做复杂逻辑推理,选数学和代码能力更强的模型。需要提醒的是,同一个任务换个模型,Agent 行为差异可能非常大——原厂指令跟随好的模型在工具选择上要准确得多。这个只能实际跑一轮日志对比,没有捷径。
2.2 工具定义:写 docstring 比写实现更耗时
Agent 的工具注册方式有几种,我最常用的是@tool装饰器,把一个普通 Python 函数包装成 Agent 可以调用的工具。这里有个非常容易被低估的点:工具的函数签名、参数类型提示、docstring,才是 Agent 理解这个工具的入口。模型并不会读你函数内部怎么实现,它只看你暴露出来的描述和签名。
我举一个例子。假设我要做一个资讯汇总的工作流,需要两个工具,一个负责抓取公开资讯,一个负责把结果保存成文件:
from langchain.tools import tool @tool def fetch_news(topic: str, max_count: int = 5) -> str: """根据主题 topic 获取最新的公开资讯列表。 Args: topic: 要搜索的主题关键词,尽量具体。 max_count: 返回的资讯条数,默认为 5。 Returns: 返回格式化文本,包含标题和来源摘要,字段用换行分割。 """ # 这里填你实际的抓取逻辑,比如调用某个公开搜索接口 news_list = search_news(topic, max_count) return "\n".join( f"标题:{item['title']};来源:{item['source']}" for item in news_list ) @tool def save_report(content: str, file_name: str) -> str: """把最终报告内容 content 保存为本地 Markdown 文件。 Args: content: 完整的 Markdown 文本内容。 file_name: 文件名,不要包含路径分隔符和扩展名。 Returns: 返回保存后的完整路径。 """ path = f"./reports/{file_name}.md" with open(path, "w", encoding="utf-8") as f: f.write(content) return path我踩过的坑是,刚开始写工具描述过于随意,比如 "search info",结果 Agent 完全搞不清楚该传什么参数。后来我强制自己按"功能描述 + 参数说明 + 返回值说明"三段式来写 docstring,Agent 调用工具的准确率明显上升。一定要多写"什么时候该用、参数里有哪些坑"。比如save_report如果文件已存在要不要覆盖,最好在 docstring 里说明,否则模型不会知道。
另外一个细节是:工具返回内容不要过长。Agent 每次拿到 Observation 都会拼进 prompt 里,要是工具返回一个 2 万字的文档,两轮循环下来上下文就爆了。比较好的做法是在工具内部先做摘要或截断,只返回关键信息。比如抓取资讯,不要返回全文,返回标题、来源、链接就够了。
2.3 Prompt 设计:怎么跟 Agent 说清楚它该干嘛
Agent 的 prompt 比普通 Chain 复杂的地方在于,它内部已经有一套 ReAct 格式模板,你需要做的是在create_react_agent里传入你的自定义系统消息,把自己对任务的约束加进去。我一般会写这几类内容:
- 身份与目标:你是一个自动化研究员助手,负责根据给定主题完成信息收集并输出报告。
- 工作步骤建议:先收集信息,再判断信息完整度,必要时补充查询,最后输出报告。不要跳过收集阶段。
- 输出格式硬约束:最终报告必须使用 Markdown,包含摘要、要点、结论三个部分。
- 边界警示:如果工具多次返回空结果,直接输出"未找到有效信息",不要编造内容。
这里最忌讳的是把 prompt 写得像需求文档,一条一条列一堆,模型反而丢失重点。我自己的做法是:把约束压到不超过 5 条,每条都是"可验证"的行为要求,比如"保存文件前需要先调用 save_report 工具,不要在 Final Answer 里伪造路径",这种就比"请确保正确性"有用得多。
2.4 记忆模块:工作流要不要带记忆功能
自动化工作流有两种形态。一种是单次任务型:用户丢一个主题,Agent 跑完就结束,不需要记忆。另一种是持续对话型:比如企业内部"知识助手",会连着聊很多轮,需要记住用户提过什么。LangChain 里给 Agent 加记忆是通过把历史消息回传给执行器实现的,最基础的方式是用memory属性传一个消息列表。
但我劝你一句:简单自动化流程尽量别加记忆。记忆意味着每轮对话都要携带全部历史记录,上下文膨胀非常快。我见过一个小项目,加了对话记忆后 token 消耗翻了 3 倍,Agent 反而变蠢了,因为它被大量历史对话干扰了判断。如果一定要用记忆,推荐用带有摘要压缩能力的记忆类型,或者只保留最近 K 轮消息。纯粹跑批处理的 Agent,别在记忆上花心思。
3. 实操过程:搭建一套"信息收集 + 报告生成"自动化 Agent
3.1 环境准备与依赖安装
开始写代码之前,先把环境准备好。我用的是 Python 3.10 以上版本。LangChain 这两年版本更新挺频繁,建议新项目直接装最新稳定版,避免用旧版本踩一些已经被修复的坑。
pip install -U langchain langchain-core langchain-community如果你的 Agent 需要接某个模型服务,还需要按对应服务商的集成包安装,具体包名可以参考 LangChain 官方集成文档。我自己的习惯是尽量把网络请求超时时间设短一点,避免工具调用卡死导致整个流程挂掉,后面会展开说。
装完可以先跑一个最小验证:直接创建 Agent 不带任何工具,让它回答一个简单问题,确认模型接入正常。从最简单的开始,别一上来就搞一堆工具,出问题都不知道是哪一环。
3.2 构建一个可复用的 AgentExecutor
下面这段代码我实际用的频率很高,是一个"报告生成 Agent"的基础骨架。它的任务是:根据用户输入的主题,调用两个工具(收集资讯、保存报告),然后输出最终结果。
from langchain.agents import create_react_agent, AgentExecutor from langchain_core.prompts import PromptTemplate from langchain_core.tools import tool # 这里用你实际接入的模型实例 # 明确要求:需要支持工具调用,否则 Agent 无法正常工作 llm = init_chat_model() # 工具定义略,沿用上文 fetch_news / save_report tools = [fetch_news, save_report] system_prompt = """你是一名自动信息采集与报告助理。 你的任务是根据用户提供的主题,完成以下工作: 1. 调用 fetch_news 收集该主题的公开资讯。 2. 如果收集到的信息足够,调用 save_report 保存一份 Markdown 报告。 3. 在最终回答中只输出文件路径和一句话摘要,不要重复报告全文。 约束: - 不要编造不存在的资讯内容。 - 如果 fetch_news 返回空结果,直接说明未找到信息,不要继续调用 save_report。 - 报告必须包含“摘要”“核心要点”“结论”三个章节。 """ agent = create_react_agent( llm=llm, tools=tools, prompt=PromptTemplate.from_template(system_prompt), ) executor = AgentExecutor( agent=agent, tools=tools, verbose=True, max_iterations=6, handle_parsing_errors=True, early_stopping_method="force", )调用方式很简单:
result = executor.invoke({"input": "帮我整理一下最近关于新能源汽车市场的信息,并生成报告"}) print(result["output"])这里面有几个参数我要单独拎出来讲。
max_iterations=6是硬性循环上限。我给的是 6,因为这里只需要"收集 + 写入"两步,正常两三轮就结束了。设成 6 是为了兜底,防止 Agent 反复调整工具参数。如果你任务复杂,可以提高到 10 甚至 15,但一定要结合 token 成本考虑。
handle_parsing_errors=True很关键。Agent 返回的 Action 格式偶尔会不合法,比如把参数写错、引号没闭合、输出了一堆多余文字。开启这个参数后,LangChain 会把解析错误反馈给模型,让它自己纠正,而不是直接抛异常退出。实测下来,开启之后任务成功率能从 70% 提到 90% 以上。
early_stopping_method="force"的语义是达到迭代上限后直接终止,以最后的输出兜底返回。尤其适合自动化批处理,至少能保证不卡死。
3.3 多 Agent 协作编排:拆任务比揉一个大 Agent 更稳
做一个稍微复杂一点的工作流时,我还发现一个规律:与其把所有工具都塞进一个 Agent,不如拆成多个职责单一的 Agent,再用代码把它们串起来。模型做决策的准确率会随着工具数量增加而下降。工具少而精,决策才稳。
举个例子,我之前做"周报生成"不是单个 Agent 一把梭,而是拆成两层:
第一层是"数据收集 Agent":只负责读各类数据源,把分散的指标整理成统一的结构化 JSON。它的工具只有 2 个:数据库查询、表格解析。
第二层是"写作 Agent":拿到结构化 JSON,参考上一周的记录,生成周报 Markdown。它的工具只有 1 个:保存文件。
数据通过代码传给下一个 Agent,而不是让 Agent 自己跨层调用。这样做的好处非常明显:第一层跑错数据了,我可以在中间层做校验,不合格就直接触发重跑,而不用等第二层模型瞎发挥。编排代码大致这样:
data_agent_result = data_executor.invoke({"input": "统计所有渠道本周关键指标"}) raw_data = data_agent_result["output"] validated_data = validate_and_fallback(raw_data) write_result = write_executor.invoke({ "input": f"根据以下数据生成周报:\n{validated_data}" })这里validate_and_fallback是我自己写的一个校验函数,比如解析 JSON、检查必填字段、数值是否在合理范围内。这些"笨办法"往往比让模型自己去判断靠谱得多。多 Agent 编排不是要让 Agent 之间互相通信,而是让代码做运输、Agent 做决策,分工才清晰。
3.4 回调监控:不打开日志的 Agent 就是盲盒
Agent 跑偏了,第一件事永远是看它每一步在想什么。所以我强烈建议,任何 Agent 工作流刚搭建的时候,verbose=True一定要开着。它会打印完整的 Thought / Action / Observation 循环日志。
到了生产环境,verbose 日志可以关掉,但回调(Callback)必须加。LangChain 提供了一套回调机制,我常用的是在每一步动作发生时把它记录下来,存成结构化日志,方便事后回溯。
from langchain_core.callbacks import BaseCallbackHandler class AgentTraceHandler(BaseCallbackHandler): def on_agent_action(self, action, **kwargs): log_entry = { "tool": action.tool, "tool_input": action.tool_input, "step": len(self.steps) + 1, } self.steps.append(log_entry) print(f"[Agent] 第{len(self.steps)}步 -> 调用 {action.tool}") def on_agent_finish(self, finish, **kwargs): print(f"[Agent] 完成: {finish.return_values}") trace_handler = AgentTraceHandler() executor.agent.callbacks = [trace_handler]这个日志文件就是排查问题的第一手证据。哪一步开始胡说八道、哪个工具调用参数错得离谱、是不是在同一个工具上反复横跳,全部一目了然。后面排查问题的时候,几乎所有场景都要回到这个 trace 里找答案。
4. 常见问题与排查技巧实录
4.1 Agent 陷入死循环,同一个工具反复调用
这是出现频率最高的问题。现象是日志里 Agent 一直在调用同一个工具,参数只做一点点微小变化然后结果几乎一样,始终不输出 Final Answer。
我的排查顺序是:
- 先看是不是工具返回信息不够,Agent 总觉得"我还缺一点"。如果是,检查工具返回是否包含 Agent 决策所需的最关键字段,或者明确在 docstring 里提示"返回结果已经完整,无需重复查询"。
- 再看是不是 prompt 里的目标不清晰,Agent 不知道做到哪一步才算完成。这时候要在 system prompt 末尾加一句:"当你认为信息已充分或已达到任务目标时,必须直接停止调用工具并输出 Final Answer。"
- 最后检查
max_iterations,如果设得太高(比如 20 以上)且没有任何兜底,Agent 就更容易放飞。我一般先用 5~8 做测试,观察正常流程需要的轮次,再留 50% 余量。
另外还有一个玄学但确实有效的方法:在工具返回内容里加一行"如果本信息已足够,请直接结束任务"。模型对这种显式提示的接受度很高。
4.2 工具参数解析错误,模型不按 JSON 格式输出
报错长这样:Could not parse LLM output或Error: Invalid or incomplete response。通常是模型生成的 Action Input 不符合 JSON 格式,比如用了单引号、末尾多了逗号、或者干脆把参数写在 Action 后面没套 JSON。
这个问题的根源多半是模型对工具描述理解不到位,或者工具参数太复杂。我给的解决方案有三个方向:
- 简化工具参数。能传字符串就不要传嵌套对象,能传三个参数就不要传五个。参数类型提示写明,一定要用
int、str、float这些基础类型,别用 Pydantic 深层模型。 - 开启
handle_parsing_errors=True,让模型收到解析错误信息后自我纠正。这个开关不是万能,但能救回来一半以上。 - 在工具 docstring 里给一个调用示例,比如
fetch_news(topic="新能源汽车", max_count=5),模型会照着格式抄。
还有一种情况是模型不支持工具调用,强行用了 ReAct 格式,导致输出不稳定。这种情况只能换模型,没有别的办法。
4.3 上下文窗口溢出,多跑几轮就报 Context Length Exceeded
Agent 把历史步骤反复塞进模型的上下文里,这是它的原生特点,很难完全避免。出现溢出时我一般从三个维度控制:
- 控制工具返回长度。这是最有效的一招。几乎每个工具都该做输出截断,最多返回几百个 token 的关键信息。如果 Agent 需要全文细节,就改成把全文写入临时文件,工具只返回文件路径。
- 控制迭代次数。减少
max_iterations,或者设计 more like"尽早收敛"的 prompt 提示。每少一轮循环,就少一轮完整历史进上下文。 - 换长上下文模型。如果业务必须得多步迭代,直接选上下文窗口大的模型,不要在一个窗口小的模型上硬扛。
另外,LangChain 有些记忆组件能自动总结历史,但这是给对话场景用的,批处理工作流里我不太推荐,总结本身也消耗 token 和延迟。
4.4 Token 消耗不可控,一次任务跑出天价账单
有个项目我第一次跑完整流程,账单吓我一跳——一个简单任务烧了 50 万 token。原因就是 Agent 反复试错,每个 action 都带上了全部历史,且工具返回了超长文本。
成本控制本质上要从"减少无效循环"和"减小单轮体积"两个方向下手。我的习惯是:
- 给工具返回加"信息预算"。比如最多 10 条资讯、每个摘要不超过 20 字、不返回正文。
- 开
max_iterations=5而不是默认的 15。 - 在 prompt 里明确要求"首次使用工具时,尽量使用准确的搜索关键词,避免多次无意义修正"。
- 对可以缓存的工具结果加一层缓存装饰器,同一主题下次跑直接复用。
还有个小技巧:开发调试阶段用本地小模型或者便宜模型跑通逻辑,最后再用生产模型执行。很多 Agent 行为调试不需要大模型能力,小模型也能暴露格式问题。
4.5 输出不稳定,同一输入两次结果不一样
这个问题是 LLM 天然随机性带来的,无法消除,但可以抑制。工作流的输出不稳定多数是因为 Agent 最终回答没有强约束,它自由发挥。我的办法是把"最终输出"收敛成"工具的输出"。
举个例子:我需要生成一个固定格式的 JSON 结果,就不让 Agent 自己输出 JSON,而是给它一个format_result工具。Agent 只需要提取好数据和主要结论,然后调用这个工具去落地格式,模型自己不会碰 JSON 结构。因为工具输出的格式是代码控制的,绝对稳定。
更极端的做法是:Agent 只负责收集信息,所有最终内容的渲染、排版、包装都由代码完成,模型只提供原材料和摘要判断。这套思路可以降低八成输出稳定性问题。记住一个原则:能用代码保证的确定性,不要交给模型的随机性。
5. 最后再分享一点我的实际体会
我在多次项目里用 LangChain Agents 做自动化工作流,最大的转变是认知上的:别再试图用 Agent 替代所有业务逻辑,而是把它当作一个"聪明的调度器"。调度器可以灵活,但真正的确定性操作(文件保存、格式转换、数值计算、权限控制)还是得交给传统代码。这样组合下来,流程既灵活又可靠。我现在遇到新需求,一般会先问自己三个问题:这个任务有明确答案吗?工具足够可靠吗?失败能兜底吗?三个问题都有解,才值得上 Agent。
如果你正准备搭建自己的第一个 Agent 工作流,我建议不要上来就做多 Agent 协作,也不要追求工具数量多,先从一个极其垂直的小场景开始,跑通一次完整的"思考—调用—观察—输出"循环。等你能预测 Agent 在大多数情况下会做什么了,再一点点拓宽边界。这活儿不复杂,但需要耐心。