在开发agent的rag过程中遇见的问题和解决方案
一、环境与依赖报错类
报错:create_react_agent 不识别 state_modifier 参数
问题:当前 langgraph-prebuilt==1.0.8 属于版本重构过渡期,仅支持 prompt 参数,未上线 state_modifier。
方案:将传参改为 prompt=系统提示字符串,不要用 SystemMessage 对象。
报错: NameError: name ‘Optional’ is not defined
问题:未导入 from typing import Optional,类型注解无法识别。
方案:导入对应类型包,补齐类型导入语句。
二、工具调用异常类
错误:常识问题(1+1)频繁错误调用知识库 RAG
初始问题:写死了前置强制 RAG 检索
方案:手动模拟agent,应该迁移到langchain的原生agent(create_openai_tools_agent和AgentExecutor)
中间错误:ImportError: cannot import name ‘AgentExecutor’ from ‘langchain.agents’
原因:create_openai_tools_agent、AgentExecutor已经从主包langchain 正式移除,被归类到遗留兼容包langchain-classic,官方不再推荐在新项目使用LangChain;仅适用于0.x版本的。
LangChain 1.x 官方主推两种方案:
方案 A(兼容旧代码):安装langchain-classic拉回遗留 API;
方案 B(官方推荐、长期维护):迁移到langgraph.prebuilt.create_react_agent
后续错误:依旧没解决
问题:①仅单工具,开源模型习惯性优先调用工具;②约束规则同时写在系统提示词、工具描述两处,指令稀释;③温度存在随机性,指令遵循不稳定。
方案:规则统一放系统提示词,精简工具描述;temperature=0;增加接口前置拦截 + 工具内二次拦截双重兜底。
错误:明明写了禁止调用,模型仍触发工具链路,仅拦截了向量检索
问题:拦截逻辑写在工具函数内部,只能跳过数据库检索,无法阻止模型生成工具调用决策。
方案:在 Agent 执行入口最前端做正则拦截,命中数学类问题直接返回结果,不走 Agent 流程。(试了没用,未解决)
错误:Agent 检索文档无结果,兜底 RAG 可以正常查到文档
问题:误用全局@tool定义工具,无法动态传入top_k、相似度阈值,只能使用默认参数,和 RAG 检索配置不一致。
方案:保留原有_run闭包 +StructuredTool.from_function写法,每次请求动态注入检索参数,保证两端配置完全一致。
误解:工混淆工具名称字符串与函数参数
问题:误以为工具名称字符串会继承底层search_knowledge_base多参数签名,担心参数不匹配。
方案:name仅为工具标识字符串,对外入参由_run函数决定,仅暴露 query,,其余业务参数通过闭包注入,对模型不可见。
三、提示词与模型输出幻觉类
错误: 模型【思考】文本写调用工具,实际未触发工具执行,(格式幻觉)
问题:强制要求模型在自然回复内手写工具调用 JSON,混淆文本输出和结构化 Function Calling,出现格式仿写幻觉。
方案:取消手写 JSON 格式要求,仅让模型用文字说明决策理由;以后端tool_used、检索耗时、文档来源作为是否调用工具的唯一判定依据。
错误: Python 通用编程问题本该调用工具却未触发
问题:通用编程属于全网公开知识,模型判定无需检索;叠加格式幻觉仅仿写调用话术,未发起结构化工具调用。
方案:提示词明确划分边界:仅企业内部私有业务文档允许调用工具,公开常识禁止调用;优化输出格式,避免格式类幻觉,禁止手写工具调用 JSON。
四、代码架构选型类
报错:大模型选型错误:ResponseError: registry.ollama.ai/library/llama3:instruct does not support tools (status code: 400)
原因:基础模型llama3:instruct 本身并不支持原生的工具调用(Function Calling / Tools)。它无法理解 API 请求中包含的 tools 参数,因此返回了 400 错误。
方案:把模型换为 llama3.1:8b(ollama拉取模型,修改代码)。 换模型之后不需要重新docker镜像,只需要重启 docker compose restart, 前提:代码是挂载形式的。
版本不同下所选择的agent实现方式不同
方案:langchain 0.x 的原生agent(create_openai_tools_agent和AgentExecutor),
和LangGraph 1.x React Agent(langgraph.prebuilt.create_react_agent) 的区别
| 维度 | LangChain 0.x 原生 Agent | LangGraph 1.x React Agent |
|---|---|---|
| 架构 | 单一 Runnable + Executor 循环 | 状态机 + 可编程图工作流 |
| 可控性 | 低(黑盒) | 高(白盒,可自定义节点/边) |
| 扩展性 | 差(难加分支/并行) | 强(天然支持复杂逻辑) |
| 调用次数限制 | max_iterations参数直接配置 | 通过recursion_limit递归上限控制 |
| 流程自定义 | 弱,执行流程固定无法修改 | 极强,可自定义节点、分支、重试、路由 |
| 流式能力 | 间接(需 hack), 中间步骤流式封装不完善, | 原生深度支持stream,可实时捕获每一步事件 |
| 对话记忆 | 简易内存记忆,扩展性差 | 原生支持MemorySaver,可持久化到 Redis / 数据库 |
| 适用场景 | 快速原型、简单任务 | 生产级、需长期维护的 agent |
| 项目定位 | 简单 Demo、老项目维护 | 企业级生产项目、复杂 Agent 业务 |
| 组件 | LangChain 0.x 原生 Agent | LangGraph 1.x React Agent |
|---|---|---|
| 核心调度组件 | AgentExecutor(执行器) | StateGraph 状态图 |
agent_executor = AgentExecutor(agent=agent,tools=tools,max_iterations=3,verbose=True) | graph = StateGraph(MessagesState)graph.add_node("agent", agent_node)graph.add_node("tools", tool_node)graph.add_conditional_edges("agent", route_func) | |
| 推理封装组件 | create_openai_tools_agent | 内置 agent/tools 节点 |
agent = create_openai_tools_agent(llm=llm,tools=tools,prompt=prompt) | 方式1(底层原生):def agent_node(state): resp = llm.bind_tools(tools).invoke(state["messages"])tool_node = ToolNode(tools)方式2(项目封装快捷版): agent = create_react_agent(model=llm,tools=tools,prompt=AGENT_SYSTEM_PROMPT) | |
| 数据存储载体 | intermediate_steps + output | messages 状态列表 |
res = agent_executor.invoke({"input":"10+20等于多少"})answer = res["output"]step_info = res["intermediate_steps"] | res = app.invoke({"messages":[("user","10+20等于多少")]})all_messages = res["messages"]final_result = all_messages[-1].content | |
| 次数限流方式 | max_iterations | recursion_limit |
agent_executor = AgentExecutor(agent=agent,tools=tools,max_iterations=3) | res = app.invoke({"messages":[("user","10+20等于多少")]},config={"recursion_limit":5}) |
| langchain 0.x 原生agent | LangGraph 1.x React Agent | |
|---|---|---|
| 核心结构 | create_openai_tools_agent:根据提示词、工具列表生成符合 OpenAI 工具调用格式的 Prompt 模板。和 AgentExecutor:Agent 执行器,负责循环调用 LLM、执行工具、限制最大迭代次数、错误处理,驱动整个工具调用流程 | 底层基于 状态(StateGraph) 实现:固定节点循环:调用LLM → 判断是否需要调用工具 → 执行工具 → 回到LLM;create_react_agent只是官方封装好的开箱即用的 React 模板,底层依然是 LangGraph 状态机。 |
| 适合版本 | j仅 LangChain 0.x 可用 (在1.x版本已移除被归类到遗留兼容包langchain-classic) | LangChain 1.x + LangGraph 0.x/1.x 官方主推方案 |
| 实现方式 | 单轮循环驱动:由 AgentExecutor 控制整个流程。 | 状态机 + 图工作流驱动:基于 LangGraph 的有向图(DAG) 构建。 |
| 执行流程 | LLM 输出一个 带工具调用格式的字符串(如 {“tool”: “search”, “args”: {…}}); AgentExecutor 解析该字符串 → 调用对应工具;将工具结果拼回提示词 → 再次输入 LLM; 重复直到 LLM 输出最终答案(无工具调用)。 | 初始化 state(含用户消息);进入循环: 调用 call_model → 检查输出是否有 tool call; 若有 → 执行 call_tool → 更新 state → 回到 call_model; 若无 → 执行 finish 节点 → 输出最终 answer。 |
| 返回数据特点 | 返回结果自带intermediate_steps:每一轮llm思考 工具调用 工具返回 的中间步骤列表,方便直接提取思考过程 工具调用记录。有统一 output 字段 直接拿到最终回答文本。整体结构封装度高,上手简单,但底层流程被封装死,自定义流程能力弱 | ① 没有intermediate_steps、output字段,所有历史交互全部存在 messages 消息列表(AIMessage/ToolMessage/HumanMessage); ② 需要遍历全部messages,区分消息类型来提取思考过程、工具调用记录、最终回复; ③ 可以搭配MemorySaver实现持久化对话记忆,支持断点续跑、流式分步捕获每一步事件 |
| 优点 | ①入门简单,中间步骤直接通过intermediate_step获取,不用遍历消息数组 ; ②可以通过max_iterations 直接限制最大工具调用轮次,防止死循环; ③老项目生态成熟,网上教程多 | ①官方未来主推技术栈,长期维护、无版本废弃风险; ②扩展性极强:可以自定义节点、条件分支、失败重试、权限拦截、多 Agent 协作; ③ 完美支持流式stream,可以实时捕获每一步思考、工具调用事件,适配前端打字输出 + 过程可视化; ④ 状态隔离完善,并发场景下请求上下文互不污染 |
| 缺点 | ① LangChain 1.x 不再原生维护,只能依赖langchain-classic兼容包,长期有废弃风险; ② 流程固定,无法自由编排分支、循环、条件判断(比如工具失败重试、多分支路由); ③ 内存、状态管理弱,复杂多轮对话容易上下文错乱 | ①上手门槛略高,需要自己遍历消息数组解析中间步骤,没有现成的intermediate_steps; ② 最大调用轮次需要通过recursion_limit配置,需要理解状态机递归原理 |
| 适用场景 | ①老旧 LangChain 0.x 线上项目维护; ② 简单单工具 RAG 场景,不需要复杂流程编排; ③ 快速验证工具调用 Demo,不想自己遍历消息提取中间步骤 | ① 需要流式输出、前端展示思考过程、工具调用过程; ② 未来可能扩展多工具、多智能体、复杂业务流程编排; ③企业级线上 RAG、Agent 生产系统 |
注意:question和 query的区别
① Query:通常指 发送给搜索引擎/数据库 的指令。它可能经过改写(Query Rewriting),比如用户问“怎么修电脑”,Query 可能是“computer repair guide”。
。LangChain 的许多组件(如 Retriever、VectorStore)强制使用 query 作为参数名。
② Question:通常指 用户的原始提问 或 LLM 需要回答的问题。
两种工具定义方式
@tool 装饰器 和 闭包 + StructuredTool.from_function 的区别
| @tool 装饰器(简洁声明式写法) | 闭包 + StructuredTool.from_function(灵活工厂写法) | |
|---|---|---|
| 原理 | @tool 是语法糖,底层会自动把函数转换成 StructuredTool,自动解析函数签名、参数、函数文档字符串作为工具描述,一行代码完成工具注册。 | 手动通过工厂外层函数接收每次请求的动态配置,利用闭包把top_k、similarity_threshold、tool_state绑定到内部_run函数,再手动封装为标准工具。对外仅暴露query一个参数给 LLM,其余业务配置对模型完全透明。 |
| 适用版本 | LangChain 0.1.x ~ 1.x 全版本兼容 | LangChain 0.x 旧版 → LangChain 1.x 新版全兼容,兼容性最强,没有版本废弃风险。 |
| 适合场景 | 工具参数固定,不需要每次请求动态配置(固定 top_k、固定阈值,不会随接口入参变化);无运行时上下文需要保存:不需要 tool_state 记录检索耗时、上次检索结果;工具逻辑简单:只做一次查询、没有前置拦截、日志埋点、权限校验等复杂逻辑;多工具批量快速开发、脚本类小项目。 | 需要每次请求动态变更工具配置:比如前端可配置 Top-K、相似度阈值;需要保存单次请求运行时状态:记录检索耗时、上次检索结果、调用次数;工具链路复杂:需要前置关键词拦截、日志打印、异常捕获、结果格式化;多用户并发场景,需要请求之间配置隔离,不能全局共用一套检索参数;企业级后端项目、RAG+Agent 线上业务系统。 |
| ✅ 优点 | 代码极简、可读性高、快速上手,原生适配 Function Calling 参数校验 | 动态参数注入,完美兼容前端可变配置;请求级上下文隔离,不会出现不同用户配置互相污染;工具执行链路完全可控,方便埋点、拦截、统一异常处理;工具名称、描述集中配置,脱离函数文档字符串,方便统一管理。 |
| ❌ 缺点 | 无法直接注入每次请求的动态参数(不能用闭包传 top_k、相似度阈值);全局定义的@tool工具会单例常驻,所有请求共用一套配置,无法隔离每次请求的运行时状态;不方便做请求级别的上下文埋点、单次调用的状态记录。 | 代码量更多,需要手动管理闭包与工具封装,入门门槛略高。 |
避坑补充:
①两种方式不能混用:同一个工具不要既写@tool又手动封装StructuredTool,会造成冗余、逻辑混淆;
②无论哪种写法,对外暴露给 LLM 的入参只能有 query,业务配置必须通过闭包 / 全局固定配置隐藏,不能交给大模型传参;
③name参数只是工具调用时的字符串标识,不会绑定底层函数的参数签名,两种方式都不会出现参数不匹配问题。
纠结:展现大模型思考过程:agent.invoke非流式, 或 stream流式
问题:非流式需要等待全流程结束才能返回结果,无法实时查看思考过程、中间步骤。
方案:需要前端实时打字输出、展示分步思考过程则改用stream;仅后端批量测试可保留invoke。
示例:
#非流式:
# 提取完整思考过程
think_steps = []
for idx, msg in enumerate(all_messages):
if isinstance(msg, AIMessage) and msg.content.strip():
think_steps.append(f"步骤{idx+1}:{msg.content}")
# 把思考过程塞进返回体,前端可以展示
return {
"reply": reply,
"think_process": "\n".join(think_steps), # 新增思考过程字段
"sources": last_result.get("sources") or [],
"search_ms": float(tool_state.get("search_ms") or 0.0),
"llm_ms": round(llm_ms, 2),
"doc_group": last_result.get("doc_group"),
"context": last_result.get("context") or "",
"mode": "agent",
"tool_used": tool_used,
}
五.渲染类
错误:代码内容渲染乱码
问题:知识库原始内容携带 HTML 标签,未经清洗直接输入 LLM,导致输出中出现 HTML 实体编码和残留标签。
方案:在工具结果格式化环节增加 HTML 反转义与正则去标签清洗步骤。
六.其他
错误: isinstance(observation, dict) 是死代码
原因:LangGraph 的 ToolMessage.content永远是字符串(即使工具返回字典,也会被序列化为 JSON 字符串)。
详述:① 普通字符串(99.9% 场景,create_react_agent 默认行为)。
② 多模态结构化字典列表 [{“type”:“text”,“text”:“xxx”}]。
langgraph.prebuilt.create_react_agent内置的 ToolNode内部会执行 str_output() 自动对工具返回值做强制序列化:
- - 如果你工具 _run 返回 dict,框架内部会自动用 json.dumps 序列化为JSON 字符串塞进ToolMessage.content;
- - 只有你手动直接构造 ToolMessage(content=[dict]) 时,content 才会是字典列表.
七.性能优化
可优化点:_run工具函数内部重复实例化ChatOllama
优化原因:每次 Agent 请求、每次_run工具函数内部重复实例化ChatOllama,会频繁创建 LLM 连接、重复加载模型上下文,带来大量重复初始化开销,造成接口响应慢、资源冗余占用。
优化方案:采用全局单例懒加载方式,把 LLM 实例放到全局作用域,只初始化一次:
定义全局私有变量_llm_instance: Optional[ChatOllama] = None
封装_get_llm()函数,只有首次调用时才创建一次模型实例,后续所有请求全部复用同一个 LLM 对象
彻底避免每次请求反复创建 LLM,减少 TCP 连接、模型上下文重复加载的性能损耗
待优化:
检索参数统一:Agent 与兜底 RAG 共用同一套top_k、相似度阈值,避免参数错误导致无效向量检索、空查询浪费算力;
前置拦截优化:在接口最开始拦截数学类常识问题,直接跳过完整的 Agent 工具调用链路,省去 LLM 推理、向量编码的耗时;
精简提示词:删除两处重复的工具约束描述,减少 LLM 的上下文输入长度,降低大模型推理耗时;
选用非流式invoke/ 流式stream按需选型,避免无用的全链路等待造成的用户体感性能差。
更多推荐
所有评论(0)