Agent检索与输出精确性提升 提高 Agent 的检索精度和输出精度核心思想只有一句话把线性 RAG改成带评估与纠错的图。单趟检索→塞给 LLM→输出必然会出现三种典型失败查询词和文档语义错位、Top-K 里混入高分低相关内容、生成时出现无依据的幻觉论断。LangGraph 的价值就是让你用节点 条件边把评估、改写、重试、人工兜底编程进流程 。一、检索精确性从相似度走向相关性传统 RAG 默认向量相似 ≈ 相关但在生产环境中这经常不成立——一篇关于 Apache 的 TLS 文章可能因为语义接近而排在 Nginx HTTPS 配置答案前面 。要从根本上提升检索精度需要在索引端和检索端同时做工。1. 索引端让被检索的块本身就是高质量的 结构感知切分Structure-Aware Chunking不要用固定的CharacterTextSplitter一刀切。Markdown / HTML 文档应当先按标题层级切再做二次递归切分这样每个 chunk 都带有section_path之类的元数据检索时能定位到具体章节 。fromlangchain_text_splittersimportMarkdownHeaderTextSplitter,RecursiveCharacterTextSplitter header_splitterMarkdownHeaderTextSplitter(headers_to_split_on[(#,h1),(##,h2),(###,h3)])sub_splitterRecursiveCharacterTextSplitter(chunk_size800,chunk_overlap100)defchunk_documents(raw_docs):all_chunks[]fordocinraw_docs:md_chunksheader_splitter.split_text(doc)forchunkinmd_chunks:iflen(chunk.page_content)800:sub_chunkssub_splitter.split_documents([chunk])all_chunks.extend(sub_chunks)else:all_chunks.append(chunk)returnall_chunks参数经验值来自生产实践中文通用文档chunk_size500, overlap100技术文档/论文chunk_size800, overlap150法律条款chunk_size800, overlap200按第X条切代码chunk_size200, overlap30overlap 的作用至关重要——一段话如果被切到中间前后两块都不完整检索时两块都匹配不上 。 父子文档切分Parent Document Retriever用小 chunk 做精确嵌入匹配但返回给 LLM 时用大 chunk 保留上下文 fromlangchain.retrieversimportParentDocumentRetrieverfromlangchain.storageimportInMemoryStore child_splitterRecursiveCharacterTextSplitter(chunk_size200)# 用于嵌入parent_splitterRecursiveCharacterTextSplitter(chunk_size2000)# 用于返回retrieverParentDocumentRetriever(vectorstorevectorstore,docstoreInMemoryStore(),child_splitterchild_splitter,parent_splitterparent_splitter,) 元数据注入每个 chunk 必须带doc_source、time_updated、section_path。这些是后续做元数据过滤如filter{region: EMEA, department: IT}的基础 。2. 检索端混合检索 查询改写 重排序 混合检索Dense Sparse纯向量检索在术语精确匹配上会翻车如ESG碳排放等专业词。BM25 弥补了这一点fromlangchain.retrieversimportBM25Retriever,EnsembleRetriever bm25_retrieverBM25Retriever.from_documents(docs)vector_retrievervectorstore.as_retriever()ensemble_retrieverEnsembleRetriever(retrievers[bm25_retriever,vector_retriever],weights[0.3,0.7]# 语义匹配权重更高)✍️ 查询改写Query Rewriting用户的原始 query 经常含糊“手机点了坏链接怎么办”需要先改写成检索友好形式“移动设备钓鱼邮件事件响应流程”fromlangchain.promptsimportChatPromptTemplatefromlangchain_core.output_parsersimportStrOutputParser rewrite_promptChatPromptTemplate.from_messages([(system,你是一个查询改写器。给定用户问题将其改写为更适合向量数据库检索的形式使其更具体或使用不同的关键词。仅返回改写后的问题。),(human,原始问题{question}),])rewrite_chainrewrite_prompt|llm|StrOutputParser()defrewrite_query(state):rewrittenrewrite_chain.invoke({question:state[question]})return{question:rewritten,rewrite_count:state.get(rewrite_count,0)1} 重排序RerankingBi-encoder嵌入模型召回快但不精确Cross-encoder 重排能把 Query-Document 对联合打分精度显著提升 fromlangchain.retrievers.contextual_compressionimportContextualCompressionRetrieverfromlangchain_community.cross_encodersimportHuggingFaceCrossEncoderfromlangchain.retrievers.document_compressorsimportCrossEncoderReranker# 先用向量库宽召回 k20base_retrievervectorstore.as_retriever(search_kwargs{k:20})# 再用 Cross-encoder 重排取 top 5cross_encoderHuggingFaceCrossEncoder(model_nameBAAI/bge-reranker-base)rerankerCrossEncoderReranker(modelcross_encoder,top_n5)retrieverContextualCompressionRetriever(base_compressorreranker,base_retrieverbase_retriever,) 进阶HyDE假设文档嵌入让用户 query 先生成一段假设性答案再用这段答案去检索能显著提升语义对齐 defhyde_retrieve(state,retriever,model):hyde_promptf请写一段可能回答以下问题的文档内容{state[query]}hypothetical_docmodel.invoke(hypothetical_doc).content docsretriever.invoke(hypothetical_doc)return{retrieved_docs:docs}二、输出精确性让 Agent 学会自我纠错光有好的检索还不够。要让最终输出精确必须在图中加入评估节点和条件重试。这是 LangGraph 相比 Chain 最大的优势——决策可编程。1. 文档相关性评分GraderLLM 对每条检索到的文档打分相关/不相关过滤掉高分低相关的噪声 frompydanticimportBaseModel,FieldclassGradeDocument(BaseModel):对检索文档的相关性评分score:strField(descriptionyes 表示相关no 表示不相关)grade_promptChatPromptTemplate.from_messages([(system,你是文档相关性评估器。判断检索到的文档是否包含能回答用户问题的关键信息。),(human,文档{document}\n\n问题{question}),])doc_gradergrade_prompt|llm.with_structured_output(GradeDocument)defgrade_documents(state):questionstate[question]documentsstate[documents]filtered[]fordocindocuments:resultdoc_grader.invoke({document:doc.page_content,question:question})ifresult.scoreyes:filtered.append(doc)return{documents:filtered}2. 幻觉检测Hallucination Check生成答案后验证答案中的每一个论断是否都能在检索文档中找到支撑 classGradeHallucination(BaseModel):score:strField(descriptionyes 表示答案完全由检索文档支撑no 表示存在无依据的论断)hallucination_promptChatPromptTemplate.from_messages([(system,判断生成答案中的每个事实论断是否都能在检索文档中找到依据。),(human,文档{documents}\n\n答案{generation}),])hallucination_graderhallucination_prompt|llm.with_structured_output(GradeHallucination)defcheck_hallucination(state):resulthallucination_grader.invoke({documents:state[documents],generation:state[generation],})return{hallucination_check:result.score}3. 答案质量评估Answer QualityclassGradeAnswer(BaseModel):score:strField(descriptionyes 表示答案切实回应了问题no 表示答非所问)answer_graderanswer_prompt|llm.with_structured_output(GradeAnswer)defcheck_answer_quality(state):resultanswer_grader.invoke({question:state[question],generation:state[generation],})return{answer_quality:result.score}4. 强制引用与 citation 校验为防止 LLM 编造引用如CITATION: [fake_source]需要在 prompt 中强制要求输出格式为 后处理校验提取答案中的所有 citation 编号确认其确实存在于state[documents]列表中不存在的 citation 一律剔除并触发重新生成5. 重试预算Retry Budget—— 防止无限循环这是生产环境最容易踩的坑。如果不设上限一个知识库中根本没有答案的问题会让 Agent 无限改写-重试 defdecide_after_grading(state):ifstate[relevance_decision]relevant:returngenerateifstate.get(rewrite_count,0)2:# 最多改写 2 次returngenerate# 用现有文档硬生成returnrewrite三、LangGraph 完整实现自纠错 RAG Agent把上面所有环节组装成一个图。状态定义如下 fromtypingimportTypedDict,List,Annotatedfromlangchain_core.documentsimportDocumentimportoperatorclassGraphState(TypedDict):question:strdocuments:List[Document]generation:strrewrite_count:inthallucination_check:stranswer_quality:strerror_reason:str图的结构START → rewrite_query → retrieve → grade_documents │ ┌───────────────┼───────────────┐ relevant not relevant rewrite_count2 │ │ │ generate ───────► rewrite_query generate │ check_hallucination │ ┌───────┼───────┐ grounded not_grounded (回到 generate) │ check_answer_quality │ ┌─────┼─────┐ useful not_useful (回到 rewrite_query 或 finish) │ FINISH核心节点实现fromlanggraph.graphimportStateGraph,END workflowStateGraph(GraphState)# 1. 查询改写节点workflow.add_node(rewrite_query,rewrite_query)# 2. 检索节点混合检索 重排defretrieve(state):querystate[question]# 这里可以注入 metadata 过滤documentsensemble_retriever.invoke(query)return{documents:documents}workflow.add_node(retrieve,retrieve)# 3. 文档评分节点workflow.add_node(grade_documents,grade_documents)# 4. 生成节点defgenerate(state):promptChatPromptTemplate.from_template(你是一个企业知识库助手。仅基于提供的上下文回答问题。\n上下文\n{context}\n\n问题{question}\n\n要求\n1. 答案必须以 [citation:N] 标注来源\n2. 如果上下文无法回答问题明确说我不知道\n3. 禁止编造信息)chainprompt|llm context\n\n.join([doc.page_contentfordocinstate[documents]])generationchain.invoke({context:context,question:state[question]}).contentreturn{generation:generation}workflow.add_node(generate,generate)# 5. 幻觉检测节点workflow.add_node(check_hallucination,check_hallucination)# 6. 答案质量节点workflow.add_node(check_answer_quality,check_answer_quality)# 边和条件路由workflow.set_entry_point(rewrite_query)workflow.add_edge(rewrite_query,retrieve)workflow.add_edge(retrieve,grade_documents)defdecide_after_grading(state):ifnotstate[documents]:returnrewriteifstate.get(rewrite_count,0)2:returngeneratereturnrewriteworkflow.add_conditional_edges(grade_documents,decide_after_grading,{generate:generate,rewrite:rewrite_query})workflow.add_edge(generate,check_hallucination)defdecide_after_hallucination(state):returngenerateifstate[hallucination_check]not_groundedelsecheck_answer_qualityworkflow.add_conditional_edges(check_hallucination,decide_after_hallucination,{generate:generate,check_answer_quality:check_answer_quality})defdecide_after_quality(state):ifstate[answer_quality]not_useful:ifstate.get(rewrite_count,0)2:returnfinishreturnrewritereturnfinishworkflow.add_conditional_edges(check_answer_quality,decide_after_quality,{rewrite:rewrite_query,finish:END})appworkflow.compile()四、人在回路Human-in-the-Loop终极精确性兜底对于高风险场景如发邮件、删数据库、对外承诺可以让 Agent 在执行工具前暂停等待人工审批 fromlanggraph.typesimportinterrupt,Commandfromlanggraph.checkpoint.memoryimportMemorySaverdefreview_tool_call(state):在工具真正执行前暂停让人审批/编辑/拒绝tool_callstate[messages][-1].tool_calls[0]human_responseinterrupt({question:批准此操作,tool_name:tool_call[name],tool_args:tool_call[args],})ifhuman_response[type]approve:return{}elifhuman_response[type]edit:state[messages][-1].tool_calls[0][args]human_response[edited_args]return{messages:[state[messages][-1]]}else:# rejectreturn{messages:[ToolMessage(content用户拒绝了此操作。,tool_call_idtool_call[id])]}# 编译时必须带 checkpointer否则无法暂停/恢复memoryMemorySaver()graphworkflow.compile(checkpointermemory)五、性能优化从 2.3s 到 380ms 的真实路径检索精确性提升后延迟往往恶化。生产环境的几个关键优化 优化项操作效果向量库分片按source_type拆分 Chroma 为多子库-42% 耗时BM25 预热高频词结果预加载到内存 dict-28% 耗时异步并行检索asyncio.gather并行多通道timeout1.2s-31% 耗时Score 预计算归一化向量化时预计算为 [0,1] 存入 metadata-15% CPUimportasyncioasyncdefparallel_retrieve(query):tasks[vector_retriever.ainvoke(query),bm25_retriever.ainvoke(query)]resultsawaitasyncio.gather(*tasks,return_exceptionsTrue)# 合并 去重 重排...六、评估没有评估的优化都是瞎忙精确性提升必须可量化。建议跟踪以下指标检索侧RecallK、MRRMean Reciprocal Rank、重排后 Top-5 准确率生成侧Faithfulness忠实度、Answer Relevancy、Citation Accuracy端到端任务成功率、人工抽检合格率使用 LangSmith 或自定义日志追踪每一步的输入输出找出瓶颈节点 。最终总结提升 LangChain/LangGraph Agent 的精确性本质是把一次性管道重构成带评估回路的图。关键认知相似度 ≠ 相关性。向量检索只是宽召回的手段真正的精度来自评估-改写-重排-校验这一整套图节点协作。检索端要做的事结构感知切分 父子文档 元数据注入混合检索BM25 向量做宽召回Cross-encoder 重排序做精准排序查询改写消除查询鸿沟生成端要做的事文档相关性评分过滤噪声幻觉检测确保每句话有依据答案质量评估确保切题强制引用 citation 校验防编造重试预算防止无限循环架构端要做的事LangGraph 状态图把上述节点编程化人在回路兜底高风险操作性能优化保证生产可用性持续评估驱动迭代最常见的三个反模式❌ 不设重试上限 → 死循环烧钱❌ 只用向量检索不用重排 → Top-K 稀释❌ 编译时不加 checkpointer → 人在回路失效按这套思路落地检索精确性和输出精确性可以同时获得质的提升——其核心在于让 Agent 具备自我怀疑和自我纠错的能力而不是盲目相信第一趟检索和第一次生成。