LangChain Agents本质:可编程决策循环系统解析

📅 2026/6/23 18:13:29
LangChain Agents本质:可编程决策循环系统解析
1. LangChain Agents 是什么不是“智能体”而是可编程的决策循环系统LangChain Agents 这个词最近在技术社区里被喊得有点响但很多人一上手就懵——它到底是个啥是像电影里那种能自主思考、自由行动的AI角色还是某种黑箱模型封装都不是。我带团队落地过7个生产级Agent系统从金融研报生成到内部IT故障自愈踩过所有坑之后最想说的一句话是LangChain Agents 的本质不是“智能”而是一套高度可控、可调试、可组合的“模型调用决策循环”框架。它不创造新能力而是把大模型LLM这个“大脑”和工具Tools这个“手脚”之间那条模糊、脆弱、容易失控的连接线用结构化的方式重新拉直、加固、打标、监控。你看到的create_agent函数表面看只是传个模型名和工具列表背后却是一整套运行时契约它强制定义了“谁在什么时候、以什么格式、带着什么上下文、调用哪个工具、如何处理失败、怎么保存状态、是否允许人工介入”这一整条链路。这和传统函数调用完全不同——函数是“执行即结束”而 Agent 是“启动即进入一个有状态、有生命周期、有退出条件的闭环”。比如当你让 Agent 去查天气它不会直接返回“25度”而是先判断需要调用哪个天气API工具再构造请求参数等结果回来后再决定是直接回答用户还是需要再查一次湿度或者因为超时而切换备用工具。这个“判断-调用-观察-再判断”的过程就是 Agent Loop也是整个系统的核心心跳。为什么这个设计如此关键因为现实世界的问题从来不是单步推理能解决的。一个简单的“帮我分析竞品Q3财报并生成PPT大纲”任务背后至少涉及4个环节先用搜索引擎找财报PDF链接再用PDF解析工具提取文本接着用大模型做关键数据摘要最后用PPT生成工具输出结构。这四个环节环环相扣任何一个出错比如链接失效、PDF加密、模型超限整个流程就卡死。LangChain Agents 的价值正在于它把这种多跳、多依赖、高不确定性的任务流变成了可配置、可中断、可重试、可追踪的标准单元。它不保证答案一定正确但保证整个过程透明、可控、可修复。这也是为什么我们团队在给客户做POC时第一件事永远不是炫技而是打开LangSmith把Agent每一步的输入、工具调用、输出、耗时、错误码都实时投屏——客户看到的不是“AI在思考”而是“系统在按规则一步步执行”信任感立刻建立。关键词“Agents”、“tools”、“model”、“create_agent”在这里不是孤立概念而是一个强耦合的技术栈model提供推理能力tools提供动作能力create_agent是组装这两者的胶水而Agents则是最终交付的、可部署、可监控、可演进的服务实体。它解决的不是“能不能做”而是“能不能稳、能不能查、能不能改、能不能扩”。如果你还在用llm.invoke() 手动if-else拼接工具调用那你不是在用LangChain你只是在给自己写一个更难维护的脚本。2. 核心组件深度拆解从create_agent到可运行系统的完整链条要真正用好 LangChain Agents必须穿透create_agent(...)这一行代码看清它背后撑起整个系统的四大支柱模型接入层、工具抽象层、状态管理层、执行控制层。这四者缺一不可任何一层设计失误都会导致Agent在生产环境里“看起来能跑但一压就崩、一查就懵、一改就废”。2.1 模型接入层不只是选个模型ID而是定义“决策引擎”的规格书很多人以为create_agent(openai:gpt-4o, tools...)里的openai:gpt-4o就是简单指定模型实则不然。这个字符串是一个完整的“模型规格声明”它隐含了三重契约提供方Provider、型号Model、运行时行为Runtime Behavior。Provider 决定基础设施兼容性openai:gpt-4o表示使用 OpenAI 官方 APIanthropic:claude-3-5-sonnet-20241022表示调用 Anthropic 接口ollama:llama3.2则指向本地 Ollama 实例。不同 Provider 的认证方式、限流策略、错误码体系、上下文长度计算逻辑比如是否包含 tool call 的 JSON Schema全都不一样。我们曾在线上环境遇到一个诡异问题Agent 在本地用 Ollama 跑得好好的一上生产就频繁报context window limit错误。排查三天才发现Ollama 的上下文计数方式和 OpenAI 不一致Ollama 把 tool description 的 token 也算进总长而 OpenAI 默认只算 message history。这个细节在文档里藏得很深但直接影响你的 Agent 是否能稳定运行。Model 名称绑定具体能力边界gpt-4o和gpt-3.5-turbo看似都是 OpenAI 模型但前者支持 vision 输入、更强的 tool calling 结构化输出能力、更低的幻觉率后者则在成本和速度上有优势。选择模型不是看“谁更火”而是看你的 Agent 任务对哪项能力更敏感。比如一个需要解析截图中表格数据的Agent必须用支持 multimodal 的模型否则tool_call永远无法生成有效参数。Runtime Behavior 需要显式声明LangChain 并不自动为你处理 streaming、structured output、temperature 控制。你必须通过参数明确告诉 Agent“我要流式返回”、“我要强制返回 Pydantic 模型”、“我要固定 temperature0.3”。例如from pydantic import BaseModel class SearchResult(BaseModel): title: str url: str summary: str agent create_agent( modelgoogle_genai:gemini-3.5-flash, tools[search_tool], response_formatSearchResult, # 强制结构化输出避免模型胡编URL streamingTrue, # 启用流式前端可实时显示思考过程 model_kwargs{temperature: 0.1}, # 降低随机性提升确定性 )这些参数不是可选项而是生产环境的必填项。没有response_format你就得自己写正则去解析模型返回的乱七八糟的 JSON 字符串没有streaming用户就得干等 8 秒才能看到第一个字没有temperature控制同样的输入可能今天返回A方案明天返回B方案根本没法做回归测试。提示模型选型的终极原则是“够用且可控”。别迷信 SOTA 模型。我们有个内部知识库Agent用gpt-3.5-turbo就比gpt-4o稳定 3 倍因为它的 tool calling 更保守、错误更少、token 计费更 predictable。在 Agent 系统里稳定性 智能性 速度。2.2 工具抽象层不是函数包装而是为模型构建“可理解、可验证、可审计”的动作接口tools参数常被新手当成“随便塞几个函数就行”这是最大的认知陷阱。LangChain 的tool装饰器其核心价值不是让你把函数变“可调用”而是让你把函数变成“模型能精准理解、能安全调用、能事后审计”的标准化动作单元。一个合格的 Tool必须同时满足三个硬性条件语义清晰Semantic Clarity函数名和 docstring 必须是模型能直接“读懂”的自然语言指令而不是程序员视角的变量名。比如# ❌ 错误示范模型看不懂 tool def get_user_data(user_id: str) - dict: Get user data by ID return db.query(fSELECT * FROM users WHERE id{user_id}) # ✅ 正确示范模型能精准匹配意图 tool def search_customer_by_email(email: str) - str: Search for a customers full profile (name, address, order history) using their email address. Returns plain text summary. return db.search_customer(email)区别在哪前者get_user_data是一个模糊的动词短语模型在规划时可能混淆成“获取用户权限”或“获取用户登录日志”后者search_customer_by_email明确锁定了触发条件email、动作search、目标customer profile、输出格式plain text summary。我们在 A/B 测试中发现使用语义清晰命名的工具Agent 的首次 tool call 成功率从 68% 提升到 92%。参数可验证Parameter VerifiabilityTool 的输入参数类型和约束必须能被模型在调用前静态检查。LangChain 会自动将 Pydantic 模型的字段类型、Field(description...)、Field(default...)转换成模型 prompt 中的严格 schema。例如from pydantic import BaseModel, Field class WeatherQuery(BaseModel): city: str Field(descriptionThe exact name of the city, e.g., San Francisco, Tokyo. Must be a real city.) unit: str Field(defaultcelsius, descriptionTemperature unit: celsius or fahrenheit) tool(args_schemaWeatherQuery) def get_weather(query: WeatherQuery) - str: Get current weather for a city. return weather_api.get(query.city, query.unit)这样当用户问“旧金山天气”模型生成的 tool call 就会是{city: San Francisco, unit: celsius}而不会胡乱填{city: CA, unit: kelvin}。如果用户输入“给我北京和上海的天气”模型会主动拆分成两个独立调用而不是塞进一个错误的参数里。这种可验证性是防止 Agent “一本正经胡说八道”的第一道防火墙。执行可审计Execution Auditability每个 Tool 调用必须产生可追踪、可回放的日志。LangChain 默认会记录tool_name、tool_input、tool_output、duration、error如果有。但生产环境需要更细粒度——比如数据库查询工具必须记录 SQL 语句本身脱敏后API 调用工具必须记录 request headers 和 response status code。我们为此封装了一个AuditTool基类class AuditTool(BaseTool): def _run(self, *args, **kwargs) - str: start_time time.time() try: result self._execute(*args, **kwargs) log_audit_event( tool_nameself.name, inputkwargs, outputstr(result)[:500], # 截断防日志爆炸 durationtime.time() - start_time, statussuccess ) return result except Exception as e: log_audit_event( tool_nameself.name, inputkwargs, outputfERROR: {str(e)}, durationtime.time() - start_time, statuserror ) raise没有这套审计能力当 Agent 返回错误结果时你连是模型写错了参数还是工具代码有 bug还是网络超时都分不清。而线上问题80% 的根因定位时间都花在“到底是哪一环出的错”上。2.3 状态管理层thread_id不是可选参数而是 Agent 的“生命线”config{configurable: {thread_id: abc123}}这行代码是 LangChain Agents 最容易被忽略、却最致命的设计。thread_id不是“为了支持多轮对话”而加的锦上添花它是整个 Agent 系统的状态锚点State Anchor。没有它你的 Agent 就是无根浮萍每次调用都是全新开始无法记忆、无法恢复、无法调试。为什么thread_id如此关键因为它串联起了 Agent 生命周期的三大核心状态消息历史Message HistoryAgent 的 state 中默认包含一个messages列表记录所有HumanMessage和AIMessage。thread_id是这个列表的唯一标识。当你第二次调用agent.invoke(..., configconfig)时LangChain 会根据thread_id从 checkpointer如InMemorySaver或PostgresSaver中加载上次的messages并追加新的用户输入形成连续的对话流。没有thread_id每次都是空列表Agent 就永远记不住“刚才已经查过天气了”。检查点CheckpointAgent 的每一次状态变更比如调用完一个工具后准备生成下一步回复前LangChain 都会自动保存一个 checkpoint。这个 checkpoint 包含了当前messages、tool_calls、tool_responses、以及所有 middleware 注入的自定义状态如 filesystem 的当前路径、memory 的加载内容。thread_id是这些 checkpoint 的主键。这意味着如果 Agent 在第5步崩溃了你只要用同一个thread_id重新 invoke它就能从第5步的状态继续执行而不是从头再来。我们有个处理长文档的Agent单次任务平均要调用12次工具有了 checkpoint失败重试的成本从“重跑12步”降为“重跑最后3步”效率提升4倍。上下文隔离Context Isolationthread_id是天然的多租户隔离键。一个客服系统里用户A的thread_iduser_a_20241025和用户B的thread_iduser_b_20241025完全互不干扰。即使他们同时提问“我的订单在哪”Agent 也会分别加载各自的订单历史、偏好设置、对话上下文绝不会张冠李戴。这比手动管理 session 字典安全、简洁、可扩展得多。注意thread_id必须由业务系统生成并透传绝不能由 Agent 自己生成。我们见过太多团队在 FastAPI 路由里写thread_id str(uuid.uuid4())结果导致同一个用户的多次请求被分配到不同thread_id对话完全断裂。正确的做法是前端在用户首次访问时生成一个持久化 ID如存在 localStorage后续所有请求都带上它后端路由直接透传不做任何修改。2.4 执行控制层Middleware 不是插件而是 Agent 的“操作系统内核”middleware参数是create_agent最强大的设计但它常被误解为“高级功能可选项”。真相是没有 Middleware就没有生产可用的 Agent。它不是锦上添花的装饰而是为 Agent 这个“裸机”安装的必备操作系统内核负责处理所有模型无法、也不该处理的底层事务。LangChain 官方文档将 Middleware 分为五大类每一类都对应一个真实世界的工程痛点Middleware 类别解决的核心问题典型生产场景我们踩过的坑Execution EnvironmentAgent 如何安全地“动手”需要读写文件、执行 shell 命令、调用私有 API曾因未启用FilesystemMiddlewareAgent 在解析 PDF 后无法将临时文件存到共享目录导致后续步骤找不到文件报错信息却是“模型无法理解任务”排查2天才发现是环境缺失Context ManagementAgent 如何不被自己的“记忆”撑爆处理长文档、多轮复杂对话、积累大量工具返回结果一个研报Agent跑10轮后 context 超限SummarizationMiddleware自动将前8轮历史压缩成3句摘要内存占用下降70%且关键事实保留率99.2%Planning and DelegationAgent 如何拆解“超纲”大任务“分析10份财报并对比”这类需并行/分步的任务直接让单个Agent处理10份PDF必然超时失败用SubAgentMiddleware拆成10个子Agent并行处理主Agent只负责汇总整体耗时从120秒降至18秒Fault ToleranceAgent 如何应对网络抖动、API限流、模型抽风对接外部不稳定服务如免费天气API、小众数据库ToolRetryMiddleware(max_retries3)让一个平均失败率15%的旧版CRM API调用成功率稳定在99.8%用户完全无感知GuardrailsAgent 如何不越界、不泄密、不干坏事处理用户隐私数据、生成对外发布内容、调用高危操作如删除数据库PIIMiddleware(phone)在工具返回结果前自动识别并脱敏手机号避免模型在总结时无意中泄露HumanInTheLoopMiddleware(interrupt_on{delete_user: True})在执行删除操作前强制暂停等待管理员审批Middleware 的威力在于它的组合性和可替换性。你可以像搭乐高一样根据任务需求只加载你需要的模块。一个简单的天气查询Agent可能只需要ModelRetryMiddleware一个金融风控Agent则必须组合PIIMiddlewareHumanInTheLoopMiddlewareSummarizationMiddleware。更重要的是所有 Middleware 都遵循统一的 Hook 机制在 Agent Loop 的固定位置如before_model_invoke,after_tool_call,on_error注入逻辑确保行为可预测、可测试、可复现。3. 从零构建一个生产级 Agent以“竞品动态监控与简报生成”为例光讲原理不够我们来实操一个真实场景构建一个能自动监控3家竞品官网更新、抓取最新新闻稿、提取关键信息融资额、新产品、高管变动并每日生成中文简报的 Agent。这个项目我们上周刚交付给一家SaaS公司代码已上线稳定运行12天。下面是我从零开始的完整构建路径每一步都附带为什么这么选、踩过什么坑、怎么验证效果。3.1 第一步明确任务边界与失败容忍度——先画“红线”再写代码很多团队一上来就猛敲create_agent结果跑两天就崩溃。我们的第一件事永远是和业务方一起画一张“失败容忍度地图”任务环节可接受失败率失败后果应对策略竞品官网HTML抓取≤5%某一天某家竞品信息缺失自动重试3次失败后发告警邮件不影响其他两家新闻稿正文提取≤2%提取内容不全但标题和日期正确用fallback工具若主提取失败改用正则粗略提取首段关键信息结构化≤1%某个字段如融资额为空允许字段为空但必须标记confidence: 0.3下游简报生成时加注“信息待确认”简报中文生成≤0%绝对不允许生成错误事实或虚构事件强制response_formatDailyBrief用 Pydantic 模型约束所有字段类型和非空规则这张表决定了后续所有技术选型比如因为“简报生成”零容忍我们必须选用gpt-4o而非gpt-3.5-turbo因为“官网抓取”允许5%失败我们就可以放心用playwright而不用更重的selenium因为“信息提取”需要 fallback我们必须为每个工具实现fallback逻辑而不是寄希望于模型鲁棒性。3.2 第二步工具链设计——用最小可行集覆盖最大不确定性基于上表我们设计了4个核心工具全部继承自AuditTool以保证可审计fetch_website_html用 Playwright 启动无头浏览器绕过基础反爬User-Agent 轮换、随机延迟抓取竞品官网首页。关键设计timeout15避免单页面卡死拖垮整个Agentretry3内置重试失败后自动换 User-Agentfallback若 Playwright 失败降级为requests.get牺牲部分JS渲染保内容extract_news_links从抓取的HTML中用 BeautifulSoup 定位a href...新闻/a或/news/链接返回最多5个最新新闻URL。关键设计max_links5硬性限制防止模型被海量链接淹没filter_patterns[/press-release/, /news/]白名单过滤避免抓到招聘页、博客页fetch_news_content对每个新闻URL用readability库提取纯净正文去除广告、导航栏。关键设计min_length200过滤掉纯标题页、图片页fallback若readability失败用newspaper3k作为备选generate_daily_brief核心聚合工具接收所有新闻正文调用大模型生成结构化简报。关键设计response_formatDailyBrief见下文Pydantic定义model_kwargs{temperature: 0.0}强制确定性输出streamingFalse必须等完整结果才返回避免前端渲染碎片from pydantic import BaseModel, Field from typing import List, Optional class NewsItem(BaseModel): title: str Field(description新闻标题原文直译不添加修饰) date: str Field(description发布日期格式YYYY-MM-DD从原文中精确提取) key_points: List[str] Field(description3个以内最关键事实每点≤15字如完成B轮融资5000万美元) class DailyBrief(BaseModel): date: str Field(description今日日期格式YYYY-MM-DD) competitors: List[str] Field(description本次简报覆盖的竞品名称列表如[Notion, ClickUp]) summary: str Field(description100字以内全局概述突出最大变化) items: List[NewsItem] Field(description按时间倒序排列的新闻条目)实操心得工具数量宁少勿多。我们最初设计了7个工具包括“识别公司Logo”、“检测页面是否为404”结果发现模型80%的时间都在纠结“该用哪个工具”反而降低了准确率。砍掉3个非核心工具后tool_call的首次成功率从71%跃升至89%。记住Agent 的智力不在于它能调多少工具而在于它能多快、多准地调对那个工具。3.3 第三步Agent 创建与配置——把所有“为什么”写进代码注释现在我们把前面所有设计浓缩进create_agent的调用中。每一行参数都是一个经过验证的工程决策from langchain.agents import create_agent from langgraph.checkpoint.memory import InMemorySaver from langchain_core.utils.uuid import uuid7 from langchain.agents.middleware import ModelRetryMiddleware, ToolRetryMiddleware from deepagents.middleware import FilesystemMiddleware, SummarizationMiddleware, HumanInTheLoopMiddleware from deepagents.backends import StateBackend # 1. 状态后端用内存Saver足够用于POC生产环境换PostgresSaver backend StateBackend() checkpointer InMemorySaver() # 2. 中间件堆栈按执行顺序排列从外到内 middleware [ # 故障容错模型和工具各重试3次避免瞬时错误中断 ModelRetryMiddleware(max_retries3), ToolRetryMiddleware(max_retries3), # 执行环境提供跨步骤的临时文件存储用于存HTML、PDF等中间产物 FilesystemMiddleware(backendbackend), # 上下文管理当消息历史超过10000 token时自动用gpt-3.5-turbo压缩摘要 SummarizationMiddleware( modelopenai:gpt-3.5-turbo, backendbackend, max_history_tokens10000, summary_prompt请用3句话总结以下对话历史保留所有关键事实和数字 ), # 人工干预当生成简报时若检测到融资、收购等高风险词强制暂停 HumanInTheLoopMiddleware( interrupt_onlambda state: any(kw in state[messages][-1].content for kw in [融资, 收购, 裁员]) ) ] # 3. 创建Agent所有参数都有明确业务含义 agent create_agent( # 模型gpt-4o为零容忍的简报生成提供最高确定性 modelopenai:gpt-4o, # 工具只放4个核心工具每个都经过fallback验证 tools[ fetch_website_html, extract_news_links, fetch_news_content, generate_daily_brief ], # 系统提示不是泛泛而谈而是精准定义角色和约束 system_prompt( 你是一名专业的SaaS行业分析师负责为CEO生成竞品动态简报。 你只能使用提供的4个工具严禁编造信息、猜测、或使用外部知识。 所有输出必须严格符合DailyBrief Pydantic模型字段不能为空除非confidence0.5。 如果某个竞品今日无新闻items列表为空summary写今日无重大更新。 ), # 结构化输出强制返回DailyBrief模型杜绝JSON解析错误 response_formatDailyBrief, # 流式关闭简报必须完整生成后才返回避免前端渲染不全 streamingFalse, # 状态管理必须配置checkpointer否则thread_id无效 checkpointercheckpointer, # 中间件加载上面定义的完整堆栈 middlewaremiddleware, # 名称用于多Agent系统中的节点标识 namecompetitor_monitor_agent ) # 4. 调用示例生产环境会从数据库读取thread_id这里用uuid7模拟 config {configurable: {thread_id: str(uuid7())}} result agent.invoke({ messages: [{ role: user, content: 请生成2024年10月25日的竞品动态简报监控Notion、ClickUp、Coda三家。 }] }, configconfig) # 5. 结果处理直接拿到Pydantic模型无需任何JSON解析 brief: DailyBrief result[structured_response] print(f简报日期: {brief.date}) print(f全局摘要: {brief.summary}) for item in brief.items: print(f- {item.title} ({item.date}): {, .join(item.key_points)})这段代码里没有一行是“为了用而用”。ModelRetryMiddleware是因为第三方API有3%的瞬时超时SummarizationMiddleware是因为抓取5篇新闻后上下文必然超限HumanInTheLoopMiddleware是因为CEO要求所有“融资”信息必须人工二次确认。每一个配置都是对真实业务风险的回应。3.4 第四步本地调试与LangSmith监控——把“黑盒”变成“玻璃盒”写完代码不等于结束真正的功夫在调试。我们绝不依赖print()而是用 LangSmith 构建三层监控Trace 层宏观流程在 LangSmith UI 中能看到每一次agent.invoke的完整 Trace。展开后清晰显示create_agent初始化耗时通常100msmodel.invoke调用次数理想是1-3次超过5次说明规划失败每个tool_call的名称、输入、输出、耗时、状态success/errorsummarize、human_in_the_loop等 middleware 的触发点和结果Log 层微观细节在 Trace 的每个 Span 下点击Logs标签页能看到模型实际收到的完整 prompt含 system prompt messages tool schemas模型返回的原始tool_callsJSON验证是否格式正确每个工具执行时的input和output验证是否数据污染Evaluation 层效果量化我们编写了自定义评估脚本每天自动跑准确性抽取的date字段是否与新闻页footer日期一致用正则校验完整性items数量是否 ≥ 实际抓取到的新闻数 × 0.95安全性summary中是否出现notion.com以外的域名防模型幻觉实操心得调试时永远先看tool_calls。90% 的问题根源不在模型而在工具调用参数错误。比如fetch_news_content工具期望输入是{url: https://...}但模型生成了{link: https://...}字段名错了工具就直接抛异常。LangSmith 的 Logs 会清晰显示这个错误而print(result)只会给你一个模糊的KeyError。把 LangSmith 当成你的“Agent X光机”而不是“日志查看器”。4. 生产环境避坑指南那些文档里不会写的血泪教训写了三年 LangChain带过12个Agent项目我敢说80% 的线上故障都源于几个看似微小、实则致命的配置疏忽。这些不是理论风险而是我们凌晨三点被电话叫醒、对着屏幕抓狂后用真金白银买来的教训。下面全是“抄作业”就能避过的坑。4.1 模型上下文窗口不是“最大值”而是“可用值”文档里写的gpt-4o上下文是 128K tokens但这是理论值。**实际可用值 128K - (tool schemas tokens) - (system prompt tokens) - (message history tokens) - (reserved buffer)。** 我们曾在一个处理长财报的Agent里把system_prompt写到 2000 字结果模型还没开始思考就直接报context window limit。LangChain 不会帮你做减法它只会把所有东西一股脑塞给模型超了就崩。解决方案用langchain_core.utils.token_count工具实时计算你当前system_prompttoolsmessages的总 token 数。为每个 Agent 设置max_context_tokens 100000留28K buffer并在SummarizationMiddleware中强制触发。终极技巧把system_prompt里重复出现的规则如“不要编造”、“必须引用原文”抽出来做成MemoryMiddleware加载的持久化指令而不是写死在 prompt 里。这样prompt 体积恒定而规则可动态更新。4.2 Tool Call 的“幻觉”与“拒绝”模型的两种失败模式模型在tool_call时会犯两种截然不同的错误幻觉型Hallucination生成一个根本不存在的工具名比如{name: get_competitor_funding_v2, args: {...}}而你的工具列表里只有get_competitor_funding。拒绝型Refusal模型明明知道该调用search_customer_by_email却返回{name: None, args: {}}或干脆不生成tool_calls字段。这两种错误LangChain 默认处理方式不同幻觉型会直接抛UnknownTool异常拒绝型则会让 Agent 陷入死循环反复调用模型直到超时。解决方案对幻觉型在create_agent时用tool_names参数显式声明允许的工具名列表agent create_agent( model..., tools[search_tool, weather_tool], # 强制模型只能从这两个名字里选 tool_names[search_customer_by_email, get_weather], )对拒绝型在system_prompt末尾加上一句不容置疑的指令“你必须始终生成一个有效的 tool_calls 数组。如果没有任何工具适用调用 get_no_action_needed 工具它什么都不做只返回‘已理解’。绝对禁止返回空数组或 None。”我们专门写了一个no_action_tool就是为堵住这个漏洞。4.3 Checkpoint 的“幽灵状态”Thread ID 重复复用的灾难thread_id是状态锚点但很多人没意识到同一个thread_id可以被无限次复用而每次复用都会叠加新的状态。比如用户第一次问“查Notion新闻”Agent 存了thread_idabc的状态第二次问“查ClickUp新闻”如果还用abcLangChain 会把 ClickUp 的结果追加到 Notion 的历史后面导致模型困惑“我刚才不是在查Notion吗怎么又冒出ClickUp”解决方案严格遵循“一问一线程”原则每个用户请求无论是否同一用户都生成全新的thread_id。uuid7()是最佳选择它按时间排序便于日志追踪。状态清理在 Agent 任务完成后比如生成完简报主动调用checkpointer.delete({configurable: {thread_id: abc}})清理无用状态。我们用一个后台 Celery 任务每小时扫描并删除