上下文工程实战:基于LangGraph与PGVector构建Agentic AI智能体

发布时间:2026/9/16 22:49:04
上下文工程实战:基于LangGraph与PGVector构建Agentic AI智能体 1. 上下文工程不是提示词工程的Plus版先说一个我观察了很久的现象很多人把上下文工程当成“更长的提示词”或者“会用几个变量拼接模板”觉得把知识库塞进system prompt就算上下文管理了。这个认知在简单问答场景下勉强能用但一旦进入Agentic AI也就是真正会自主规划、调用工具、多步推理的智能体你会发现它根本撑不住。我过去一年做了好几个基于大模型的智能体项目从销售线索清洗到私有知识库问答再到复杂业务审批流踩过的坑比写过的代码还多。最深的体会是提示词工程解决的是“单次问答中模型理解用户意图”的问题而上下文工程解决的是“在多轮交互、多工具调用、多状态流转过程中模型如何始终准确理解当前处境”的问题。这两者的复杂度完全不在一个量级。打个比方。提示词工程像给一个新人写工作说明写得清楚点新人一次任务就不会跑偏。上下文工程则是给一个团队设计一整套信息流转机制——谁负责传递什么消息、每天早会同步什么状态、出问题向谁汇报、历史决策记录放在哪、文件共享用哪个目录。你没有这套机制团队很快会乱套有人拿着过时的信息做决策有人重复干别人干过的活有人忘了自己上一小时刚说过什么。Agentic AI就是这样一支“团队”只是队员全是模型实例和工具函数。所以这篇文章我打算把上下文工程拆开揉碎来讲不搞玄学完全按照我们实际搭建Agentic AI架构时的真实路径走一遍先从底层原理说清楚为什么上下文这么难管然后给出一种经过生产验证的分层上下文架构再落到FastAPILangChainLangGraphRAGPGVector这套技术栈上的完整代码实现最后分享我们上线后遇到的那些最恶心的问题和排查方法。这套架构已经在我们内部多个智能体项目里跑了大半年不能说零故障但至少解决了“智能体理解能力飘忽不定”这个最让人头疼的问题。文章比较长建议按章节读代码可以直接抄架构思路建议结合自己的业务场景做裁剪。2. 先搞清楚问题智能体为什么总是“理解不对”2.1 上下文窗口的物理限制是所有理解问题的根源大模型的上下文窗口越来越大几百K的模型也陆续出来了不少人觉得“既然窗口这么大把所有东西都塞进去不就行了”。这个想法在玩具项目里没毛病但到了生产环境就是灾难原因有三个。第一是成本。Transformer的自注意力机制计算量是随着序列长度平方增长的虽然工程上有各种优化比如FlashAttention、稀疏注意力但API按token计费是实打实的。上下文塞得越多单次调用成本越高如果你的智能体一天要被调用上万次这个数字就非常可观了。第二是注意力稀释。模型在长上下文里的表现并不是均匀的我在实测里发现一个很典型的现象把一万行历史对话不加处理地塞进上下文模型对中段信息的提取能力会明显下降很容易忽略关键信息。这有点像一个学生在考场上看一千页的参考资料虽然都带进去了但翻不到重点等于没带。第三是“上下文污染”。你塞进来的历史记录里可能包含早期的错误判断、过时的工具返回结果、已经被用户否决的方案。这些垃圾信息如果不经过过滤就直接进入上下文模型往往会被带偏。最典型的场景是模型在第一步推理时给出了一个方向判断后面发现这个方向是错的但错误判断残留在上下文里导致模型反复在错误方向附近打转。所以上下文工程的第一步就是意识到“窗口大”不等于“什么都存”。我们需要的是一个系统性的信息筛选和传递机制而不是一个无限扩容的容器。2.2 Agentic AI对上下文的四个特殊需求普通RAG根本覆盖不了很多人一说到上下文就想到RAG检索增强生成但RAG只是上下文工程里的一小块拼图。Agentic AI对上下文的需求比“查资料-写答案”复杂得多我总结了四个最核心的维度。第一状态感知。智能体在一个多步任务中必须知道“我现在在哪一步”“我已经拿到了什么”“我还缺什么”。举个例子一个做竞品分析的智能体第一步搜索到10篇文章第二步针对其中3篇做深度阅读第三步生成报告。如果第二步的时候它已经忘了第一步究竟选了哪3篇那整个流程就断了。这种中间状态既需要程序层面的显式管理比如LangGraph里把状态存在State对象里也需要在每一步的上下文里清晰地表达给模型看。第二历史对话压缩与遗忘。长期运行的智能体比如客服智能体会产生大量历史对话这些对话不可能全部保留在窗口里。一味扔给模型会造成信息过载全丢又会让模型“失忆”。正确的做法是分层处理核心信息保留原文过程性细节压缩成摘要过时的信息主动遗忘。这一层做不好智能体就会出现“上一轮说的是A这一轮按B去处理”的精分行为。第三工具调用链的可追溯性。Agentic AI几乎没有不调工具的。调工具就涉及一个关键问题工具返回的结果、调用的参数、出错的异常信息这些是否需要放回上下文以我们踩坑的经验来看工具调用的原始输出必须放回上下文而且最好是结构化地放回。否则模型在后续推理时只能凭模糊记忆判断工具返回了什么一旦猜错整个链路就废了。第四临时性上下文与长期记忆的分离。有些信息是这一次任务需要用的比如用户这次上传的PDF内容有些是需要长期记住的比如用户公司所在的行业、用户的偏好。这两类信息如果混在一起管理长期记忆会被临时信息冲刷掉短期信息又得不到充分关注。好的架构必须用不同的存储机制和读取策略来分别处理这两类上下文。2.3 上下文工程的核心目标让模型始终处在“完全知情”的状态说了这么多问题到底什么才算“搞好了上下文工程”我自己的定义是在任意一轮交互中模型所看到的上下文应该是足够的、准确的、未污染的。足够是指能支撑当前这一步的推理决策准确是指没有过时信息和错误推测未污染是指不存在历史错误逻辑干扰当前判断。这三个要求看起来简单做起来是另一回事。因为上下文不是一个静态的数据包它是在每个节点、每次工具调用后动态更新的。这就要在架构层面设计好“上下文收集机制”“上下文写入机制”“上下文传递机制”“上下文裁剪机制”缺一环都会出问题。我在做架构设计时会把上下文工程拆成五个核心组件上下文采集器、上下文存储器、上下文构造器、上下文压缩器和上下文校验器。采集器负责从多轮对话和工具返回中抽取信息存储器负责确定信息放短期存储还是长期存储构造器负责按当前节点的需求动态组装模型要看的上下文压缩器负责对过长的历史做摘要和裁剪校验器负责剔除污染信息、防止错误数据流入模型。这套体系在这篇文章里会完整落地到代码里下面我先从整体架构讲起。3. 分层上下文架构一套经过生产验证的方案3.1 整体分层设计哪些信息放在哪一层是有讲究的我们最终采用的是一种“双栈四层”的上下文架构。所谓双栈是指短时任务栈和长期知识栈所谓四层是指系统层、任务层、会话层和记忆层。系统层是永远不变的全局设定比如智能体的角色定义、行为准则、输出格式要求、可用的工具清单。这一层的内容每次调用都要注入但内容相对稳定基本不变。任务层是当前这一次任务的目标、计划、进度状态。比如用户发来一个任务“帮我整理上周的销售数据并生成周报”任务层就会记录“目标生成周报步骤拉数据-清洗-分析-生成报告当前步骤数据清洗完成正在执行分析”。会话层是用户和智能体之间的多轮对话历史要经过压缩和筛选再进入上下文不是简单拼接。记忆层是跨会话的长期知识比如用户偏好、历史项目的总结、业务领域的背景知识一般存放在向量数据库里按需检索。这四层不是每次调用都要全部注入而是按需组装。我见过太多失败的架构统一把四层信息全部拼进一个超长prompt里效果又贵又差。正确的做法是系统层全量注入任务层状态化注入会话层压缩后注入记忆层按相关性检索后注入。这样控制每一轮上下文的体积模型理解力反而会显著提升。3.2 为什么选FastAPILangGraphPGVector这套技术栈技术选型方面我先说说我们试过什么。最早我们用纯LangChain的AgentExecutor来做问题很明显状态管理太弱跨步骤的状态只能塞进memory对象里复杂流程很容易脏而且每一步人类可介入修改的接口很别扭。后来换了LangGraph感觉终于对了——它就是为“有状态、多节点、可回退”的工作流设计的状态转移图模型比线性的Agent链强太多了。FastAPI不用多说做智能体的对外服务层几乎是标配异步性能好、Pydantic能直接和LangChain的schema对接、自动生成OpenAPI文档调试和联调都方便。PGVector这块有些人会觉得不如专业的向量数据库比如Milvus、Weaviate但我们看中的是它和PostgreSQL共存带来的运维简单性。智能体的记忆数据往往和业务数据强关联比如“这家客户的历史订单”“这个项目的过往决策记录”这些数据大概率就在PostgreSQL里。用PGVector意味着不用单独维护一套向量库同一个库里既能做业务查询又能做向量检索事务能力和备份机制还能直接复用。为什么不用更火的一些Agent框架主要原因是它们把很多流程封装得太死了。Agentic AI项目的复杂点恰恰在流程控制、状态管理和上下文组织这些细节上框架封装的越多你能改的东西就越少。LangGraph给了图级别的灵活性但又不至于让你从头造轮子这个平衡点目前来看是最舒服的。3.3 核心组件拆解状态管理器、记忆检索器、上下文组装器在这套架构里有三个组件是核心中的核心我单独拆开讲因为理解了它们后面的代码才能看得懂。状态管理器是LangGraph里的一个State对象我们定义成一个TypedDict里面包含以下字段current_node当前执行的节点名、task_goal当前任务的目标、task_plan任务执行计划、step_results各步骤产生的关键结果、tool_call_history工具调用历史、error_info最后发生的错误信息、user_context用户相关的短期上下文。设计这个状态对象时有一个关键原则状态对象里只放“程序需要用来决策的结构化信息”而把“模型需要用来理解语义的非结构化信息”放在另一份上下文对象里。为什么这么拆因为LangGraph的State是会持久化到数据库的它服务于分支逻辑判断必须轻量化、可查询。而模型要读的上下文往往是一大段自然语言如果也塞进State里整个State会变得臃肿而且每次节点间传递都有序列化开销。记忆检索器负责从PGVector里检索长期记忆。我们给每条记忆打了三个标签用户ID、业务场景、时间戳。检索的时候不是只做向量相似度检索而是先按用户ID和场景号过滤再做相似度查询。这一步看着简单但实际效果影响很大后面常见问题部分我会详细讲为什么纯向量检索在记忆场景里经常跑偏。上下文组装器是整个架构的粘合剂。它在每个节点执行前把状态管理器里的结构化信息任务进度、工具结果和从记忆检索器拿到的长期记忆用户偏好、历史总结拼装成模型最终看到的prompt。同时它会执行裁剪策略对话历史超过N轮就做摘要、工具返回结果超过N个字符就提取关键信息、过时记忆不参加组装。所有组装逻辑集中在一个地方这非常关键否则每个节点各写各的prompt拼接逻辑维护成本直接爆炸。4. 代码落地从零搭建一个Agentic RAG智能体4.1 项目目录结构和依赖准备我先给出整个项目的目录结构然后用最核心的代码逐一说明每个文件的职责。这个项目是一个“私域知识库智能助手”用户提问后智能体会先检索知识库再决定是否需要调用额外工具最后给出带引用的答案。虽然场景看起来简单但它完整覆盖了Agentic AI的典型流程你完全可以把这套骨架替换成销售助手、运维机器人或者数据查询智能体。agentic_rag/ ├── app/ │ ├── main.py # FastAPI入口 │ ├── models.py # Pydantic请求/响应模型 │ ├── agent/ │ │ ├── graph.py # LangGraph状态图定义 │ │ ├── nodes.py # 各节点逻辑 │ │ ├── state.py # 状态对象定义 │ │ └── tools.py # 工具函数检索、文档解析等 │ ├── context/ │ │ ├── assembler.py # 上下文组装器 │ │ ├── compressor.py # 历史压缩 │ │ └── memory.py # 长期记忆管理 │ └── vector/ │ ├── pgvector_store.py # PGVector存储 │ └── embedder.py # 向量化封装 ├── requirements.txt └── .env依赖方面核心是这几个包fastapi、uvicorn、langchain、langgraph、langchain-openai、langchain-community、pgvector、psycopg2-binary、pydantic-settings。如果你用的是非OpenAI的模型把langchain-openai换成对应的包就行整体架构不变。4.2 状态定义与节点编排LangGraph图设计的核心思路先直接看代码然后我解释为什么这么设计。# app/agent/state.py from typing import TypedDict, List, Dict, Any, Optional class AgentState(TypedDict, totalFalse): # 任务级信息 task_goal: str task_plan: List[str] current_step: int step_results: List[Dict[str, Any]] # 用户与对话信息 user_id: str session_id: str messages: List[Dict[str, str]] # 工具调用信息 tool_calls: List[Dict[str, Any]] last_tool_name: str last_tool_output: str # 记忆与检索 retrieved_docs: List[Dict[str, Any]] long_term_memory: str # 错误处理 error_info: Optional[str] retry_count: int这个State被设计成所有节点共享的“黑板书”。节点A往上面写内容节点B读取并继续写。节点与节点之间传递的唯一参数就是这个State对象这样设计让每一步流转都清晰可控排查问题时只需看State里各个字段的值就能快速定位。然后是状态图的构建。我们从简单场景开始先不做复杂的条件路由而是把最基础的流程走通再逐渐加分支。# app/agent/graph.py from langgraph.graph import StateGraph, END from app.agent.state import AgentState from app.agent.nodes import ( intents_node, retrieve_node, generate_node, tool_node, respond_node ) def build_agent_graph(): graph StateGraph(AgentState) # 添加节点 graph.add_node(intent_analysis, intents_node) graph.add_node(retrieve, retrieve_node) graph.add_node(tool_execute, tool_node) graph.add_node(generate, generate_node) graph.add_node(respond, respond_node) # 设置入口 graph.set_entry_point(intent_analysis) # 添加边简单版意图分析后先检索 graph.add_edge(intent_analysis, retrieve) # 检索完成后根据是否需要额外工具做条件路由 graph.add_conditional_edges( retrieve, decide_next_step, { tool: tool_execute, generate: generate, } ) # 工具执行后回到生成 graph.add_edge(tool_execute, generate) # 生成后响应 graph.add_edge(generate, respond) graph.add_edge(respond, END) return graph.compile()decide_next_step是一个条件路由函数它检查State里last_tool_name是否为空决定是继续调工具还是直接生成最终答案。这个条件是整个图最关键的分叉逻辑。4.3 上下文组装器每一轮调用模型前怎么拼Prompt状态对象是给程序看的而模型需要的是自然语言上下文这个转换就靠上下文组装器完成。我在这里没有用LangChain自带的PromptTemplate来搭一个“模板字符串”而是用函数式的方法动态构造这样灵活度更高。# app/context/assembler.py from typing import List, Dict, Any class ContextAssembler: def __init__(self, system_prompt: str, max_messages: int 12): self.system_prompt system_prompt self.max_messages max_messages def assemble( self, state: Dict[str, Any], retrieved_docs: List[Dict[str, Any]], long_term_memory: str, ) - str: parts [] # 系统层 parts.append(f## 系统设定\n{self.system_prompt}) # 长期记忆层 if long_term_memory.strip(): parts.append(f## 需要长期记住的信息\n{long_term_memory}) # 任务层当前目标和进度 task_goal state.get(task_goal, ) task_plan state.get(task_plan, []) current_step state.get(current_step, 0) if task_goal: parts.append( f## 当前任务\n目标{task_goal}\n f计划{ - .join(task_plan)}\n f当前进度第 {current_step} 步 ) # 检索层从知识库拿到文档 if retrieved_docs: doc_text \n\n.join( f[文档{i1}] {doc[content]}\n来源{doc[source]} for i, doc in enumerate(retrieved_docs) ) parts.append(f## 参考资料\n{doc_text}) # 工具结果层 if state.get(last_tool_output): parts.append( f## 工具执行结果\n f工具名称{state.get(last_tool_name, unknown)}\n f返回内容{state.get(last_tool_output)} ) # 会话层多轮对话历史 messages state.get(messages, [])[-self.max_messages:] if messages: dialogs \n.join( f{用户 if msg[role] user else 助手}: {msg[content]} for msg in messages ) parts.append(f## 对话历史\n{dialogs}) return \n\n.join(parts)这个组装器有几个细节值得说明。第一各层之间有明确的分隔标识模型能快速定位不同信息区块这比全部揉成一段让模型自己分辨要可靠得多。第二检索回来的文档带“来源”信息这既方便模型在回答时引用也方便后续排查答案是否出自知识库。第三对话历史做了截断只保留最后N轮这个N要根据业务复杂度调太少了模型看不出连续意图太多了信息过载。4.4 节点实现检索、工具调用、生成三段的完整代码下面给出三个关键节点的实现。检索节点通过PGVector检索向量数据库工具节点演示一个“查询天气”的简单工具实际业务场景里可以换成查库存、查订单等生成节点调用LLM生成最终回答。检索节点核心逻辑# app/agent/nodes.py from typing import Any, Dict from app.vector.pgvector_store import store from app.context.assembler import assembler def retrieve_node(state: Dict[str, Any]) - Dict[str, Any]: query state[messages][-1][content] user_id state.get(user_id, default) # 先做意图判断决定检索的关键词 # 这里简化处理直接用原始query检索 docs store.search( queryquery, user_iduser_id, top_k5, score_threshold0.6, ) return {retrieved_docs: docs}注意这里我们传了user_id做过滤这让检索结果因人而异同一个问题不同用户看到的参考文档会不同。这是很多RAG项目忽略的细节知识库虽然是通用的但每个用户问同一个问题时的背景不同结合用户属性过滤能显著提升相关性。工具节点逻辑def tool_node(state: Dict[str, Any]) - Dict[str, Any]: query state[messages][-1][content] tool_name state.get(last_tool_name, ) if tool_name search_web: result web_search(query) return { last_tool_output: result, tool_calls: state.get(tool_calls, []) [ {tool: tool_name, query: query, result: result} ] } if tool_name query_database: result db_query(query) return { last_tool_output: result, tool_calls: state.get(tool_calls, []) [ {tool: tool_name, query: query, result: result} ] } return {last_tool_output: 无法识别工具}工具节点的设计有一个很容易踩的坑不要把工具的原始返回直接塞进State就完事。我建议对工具结果做一层“包装”把查询参数、返回内容、执行状态绑在一起结构化存储。这样后面排查问题时你能知道“模型在这种情况下调了什么工具、传了什么参数、拿回了什么结果”而不是只看到一堆碎片。生成节点逻辑def generate_node(state: Dict[str, Any]) - Dict[str, Any]: from langchain_openai import ChatOpenAI from langchain_core.prompts import ChatPromptTemplate llm ChatOpenAI(modelgpt-4o, temperature0.2) prompt assembler.assemble( statestate, retrieved_docsstate.get(retrieved_docs, []), long_term_memorystate.get(long_term_memory, ), ) messages [ {role: system, content: prompt}, ] response llm.invoke(messages) return {last_answer: response.content}这段代码看起来平淡无奇但生产环境里我会在llm.invoke前后埋日志把prompt内容、token消耗、返回结果都记录下来。为什么因为Agentic AI的调试几乎全靠“复现当时的prompt”。没有这个日志模型答错了你根本不知道它当时看到了什么上下文排查无从谈起。这个习惯我在所有项目里都强制要求建议你也从一开始就加上。4.5 FastAPI接口层如何把智能体封装成标准服务# app/main.py from fastapi import FastAPI from pydantic import BaseModel from app.agent.graph import build_agent_graph app FastAPI(titleAgentic RAG Service) graph build_agent_graph() class ChatRequest(BaseModel): user_id: str session_id: str message: str class ChatResponse(BaseModel): answer: str sources: list[str] steps: list[str] app.post(/chat, response_modelChatResponse) async def chat(req: ChatRequest): # 构造初始状态 initial_state { user_id: req.user_id, session_id: req.session_id, task_goal: req.message, task_plan: [理解用户意图, 检索相关资料, 生成回答], current_step: 1, messages: [{role: user, content: req.message}], } # 执行图 result await graph.ainvoke(initial_state) return ChatResponse( answerresult.get(last_answer, ), sources[doc[source] for doc in result.get(retrieved_docs, [])], stepsresult.get(tool_calls, []), )FastAPI这层做了三件事接收标准化请求、调用LangGraph执行、把结果包装成标准化响应。有一个很重要的点initial_state里的task_plan不是写死的而是应该在意图分析节点里由模型动态生成。这里为了演示简化了真实项目中请务必动态生成计划否则你写死的计划和模型实际做的事一旦不匹配上下文就会产生“计划与现实矛盾”的污染。5. PGVector记忆系统让智能体真正“记得住”5.1 从业务数据库到向量记忆的映射设计长期记忆系统是智能体理解力的“底座”。我们这里选用PGVector核心表设计如下CREATE TABLE IF NOT EXISTS agent_memory ( id BIGSERIAL PRIMARY KEY, user_id VARCHAR(64) NOT NULL, scene VARCHAR(64) NOT NULL, content TEXT NOT NULL, metadata JSONB DEFAULT {}, embedding VECTOR(1536), created_at TIMESTAMP DEFAULT NOW(), last_accessed_at TIMESTAMP DEFAULT NOW() ); CREATE INDEX ON agent_memory USING ivfflat (embedding vector_cosine_ops) WITH (lists 100);注意几个设计决策。第一user_id和scene是结构化过滤字段用来缩小向量检索范围。第二metadata用JSONB存储额外信息比如来源、语义类型、重要性评分。第三last_accessed_at用来支持记忆的自动清理和强化——经常被访问的记忆说明有用长期没被访问的可以做归档或衰减。记忆写入的逻辑也很重要。我们在每一轮对话结束后会调用LLM对当前对话做一个“记忆提取”生成几条“值得长期记住的事实”。比如用户说“我们公司主营跨境电商主要在东南亚市场”对话结束后系统会生成记忆“用户所在公司主营跨境电商核心市场为东南亚”。这条记忆写入PGVector下次该用户再来提问时模型就会知道这些背景信息。5.2 记忆写入与读取的完整实现写入记忆的宿代码# app/context/memory.py from langchain_openai import OpenAIEmbeddings from app.vector.pgvector_store import store def extract_and_save_memories(user_id: str, scene: str, messages: list[dict]): 从一段对话中提取值得长期记忆的信息并写入向量库 from langchain_core.prompts import ChatPromptTemplate llm ChatOpenAI(modelgpt-4o-mini, temperature0) # 将消息拼成文本 dialog_text \n.join( f{m[role]}: {m[content]} for m in messages[-20:] ) extraction_prompt f 从下面的对话中提取值得长期记住的事实性信息。 只提取客观事实不提取主观意见只提取跨会话有用的信息不提取本次任务的一次性信息。 如果没有任何值得记忆的信息返回空列表。 对话内容 {dialog_text} 请以JSON数组格式返回每个元素包含字段 - content: 记忆内容 - importance: 1-10的整数代表重要性 只返回JSON不要返回其他内容。 response llm.invoke(extraction_prompt) memories json.loads(response.content) for mem in memories: if mem[importance] 6: # 只保存重要度高的记忆 store.insert( user_iduser_id, scenescene, contentmem[content], metadata{importance: mem[importance]}, )这个提取逻辑的巧妙之处在于记忆不是原样照搬对话内容而是经过了一次“蒸馏”把长对话压缩成了一条条干净的事实。这让长期记忆的存储体量保持在很低的水平检索效率也更高。读取记忆的逻辑def retrieve_relevant_memories(user_id: str, scene: str, query: str, top_k: int 3): 检索与当前问题相关的长期记忆 docs store.search( queryquery, user_iduser_id, scenescene, top_ktop_k, ) if not docs: # 如果场景级检索没有结果降级到用户级全局检索 docs store.search( queryquery, user_iduser_id, top_ktop_k, ) return \n.join(f- {doc[content]} for doc in docs)降级检索这个逻辑值得重视。用户在一个新场景下提问时场景级记忆库往往为空这时直接返回空会让模型失去背景信息。降级到用户级全局检索能捞出用户在其他场景留下的相关事实往往能救命。当然降级检索结果的相关性要打个折扣所以在组装器里我会在长期记忆部分加一个“来源范围”标注让模型知道这部分信息不一定和当前场景完全匹配。5.3 向量检索在记忆场景里的两个坑我提前给你踩平了第一个坑是纯向量检索忽略关键词匹配。向量相似度在语义层面很强但在精确词匹配上经常翻车。比如用户问“你们的退款政策是什么”语义上可能匹配到“退货”“售后”相关文档但如果知识库里恰好有个专门叫“退款政策”的文档向量检索可能反而没把它排前面。我的解决办法是混合检索向量检索BM25关键词检索然后把两组结果做融合排序RRF算法。在LangChain里可以直接用EnsembleRetriever实现代码很短。第二个坑是记忆之间的相互污染。假设用户A问了一个关于“如何部署Windows服务器”的问题这个记忆被存下来。下次用户A再问“如何部署Linux服务器”时向量检索可能把Windows的记忆也捞出来模型如果不够聪明可能会在回答里混入Windows的内容。解决办法有两个层面一是在记忆提取阶段就做到“信息原子的纯净性”确保一条记忆只讲一件事二是在检索时对结果做重排rerank用一个轻量模型对检索结果与query的相关性做二次打分把不相关的Top结果挤下去。我们用的是Cohere的Rerank接口效果非常明显代价就是每次查询多了几十毫秒延迟可以接受。6. 生产环境里的常见问题与排查技巧实录6.1 上下文截断后智能体突然“失忆”怎么办这是我们上线初期遇到最多的问题。现象是多轮对话超过一定长度后智能体会忘掉用户前面说过的重要信息。排查后确认是因为上下文组装器只保留最后12轮对话早期信息被物理截断了。解决方案是引入“滚动摘要”机制。具体做法是每对话N轮就调用一次LLM把前面的对话压缩成一段摘要存到State里的summary字段。后续组装上下文时把摘要和最近几轮对话一起给模型。这样既控制了上下文长度又保留了完整的信息脉络。我建议把N设为4-6轮摘要控制在200字以内。摘要的内容要格外注意策略不只是概括说了什么还要记录“用户最终确认了什么”“有什么未被解决的问题”“用户表现出什么偏好”这些是决策关键点。普通的摘要工具只总结“说了什么”带“发生了什么状态变化”的摘要才是能支撑后续决策的高质量摘要。6.2 工具调用结果进入上下文后格式混乱导致模型理解出错另一个高频问题工具返回结构化数据比如JSON直接塞进prompt后模型有时候会在回答里复读JSON或者被JSON里的字段名带偏。原因很简单模型分不清“工具返回的数据”和“应该生成的答案”之间的边界。我的处理方法是在上下文组装器里对工具返回做一层“人类可读化”转换而不是直接把原始JSON塞进去。比如数据库查询返回的是行记录我会在工具层就把它转成“共找到3条记录记录1xxx记录2xxx”这样的自然语言格式。这个转换逻辑写在哪写在tool_node里每个工具自定义自己的格式化函数。这么做还有一个额外好处token消耗会下降不少因为自然语言格式往往比JSON短。6.3 RAG检索结果太破智能体“理解跑偏”的根因排查RAG检索质量差的原因非常多我按经验给一个排查优先级。第一先看召回是不是embedding模型和知识库的领域不匹配比如医学知识库用了通用embedding效果就会差。第二看过滤是否按user_id、scene做了有效过滤没有过滤的话海量知识里捞5条命中率很难保证。第三看chunk大小我们在实际项目里试过128、256、512个token的chunk大小发现512在大多数业务场景下效果最好因为太小了语义碎片化太大了又容易混入不相关内容。第四看query本身用户问得很口语化时直接拿去检索效果差建议先让LLM把query改写成一个更标准的检索式。比如用户问“那个退款的事怎么样了”改写后是“用户咨询退款政策的处理进度”后者检索出来的文档显然更精准。上面每一条我都踩过。最典型的一次是我们给一个法律咨询智能体上线用户问“劳动合同到期不续签有赔偿吗”检索出来的却是“劳动合同签订注意事项”完全答非所问。排查下来是chunk切太小原文里明明有赔偿相关内容但被切碎后和大量无关文本混在一起向量相似度就被稀释了。后来我们把chunk改成带重叠的切法问题直接消除。6.4 一个完整的疑难杂症排查流程照着做能省半天时间最后分享一个我在所有Agentic AI项目里都用的问题排查流程简称“三层定位法”。第一层看上下文把无法复现预期的这一轮请求、状态对象、完整prompt日志捞出来人工读一遍判断模型到底看到了什么。80%的问题在这一层就能看出来——要么是上下文缺了关键信息要么是上下文里混杂了错误信息。第二层看流程如果上下文没问题那就是LangGraph的执行流程出错了看看是不是走了错误的分支条件路由函数的逻辑是不是有漏洞。第三层看工具都正常的话再检查工具调用看看工具返回的数据本身是否准确、是否有异常、是否有缺失字段。这套流程我建议做成一个小脚本输入一个session_id自动输出该session在某次对话时的完整状态快照和prompt日志排查问题时效率提升非常明显。很多人一上来就怀疑模型不行其实大多数问题都是上下文工程没做到位模型只是个背锅的。我个人在实际操作中的体会是Agentic AI项目的成功与否七分在上下文工程三分在模型能力。模型选贵一点便宜一点差别是体验上的但上下文没设计好模型再强也发挥不出来。这篇文章里的架构和代码是我在多个智能体项目里反复打磨后沉淀下来的通用骨架你用的时候不需要照搬但建议把分层思想、状态持久化、组装器集中化、日志留存这几件事一定做扎实。踩过几次坑之后你会更认同这句话上下文工程不是提示词工程的Plus版而是一套需要认真对待的系统工程。