LangChain生产实战:LCEL链式编程与RAG检索优化指南

发布时间:2026/7/8 19:22:10
LangChain生产实战:LCEL链式编程与RAG检索优化指南 1. 这不是“手册”是我在生产环境里踩出的LangChain上手路径LangChain不是一本翻完就能合上的说明书它是一套需要你亲手拧紧每一颗螺丝的工具箱。过去两年我用它搭过三套企业级RAG系统、两个智能客服中台、一个合规文档自动审查Agent也经历过凌晨两点对着auto-compaction failed (context overflow: prompt too large for the model)报错反复重启服务的崩溃时刻。所谓“看完就能上手”绝不是指照着API文档敲完几行代码就宣告成功——而是当你第一次把用户模糊提问“去年Q3华东区客户投诉最多的三个产品问题是什么”准确映射到数据库字段、召回相关工单、过滤非投诉类记录、再结构化输出成带编号的摘要时那种手指悬在回车键上、心跳加速的真实感。LangChain的核心价值从来不在它提供了多少类和方法而在于它把原本散落在不同模块里的“意图理解—数据获取—逻辑编排—结果生成”这一整条链路用统一的抽象Chain、Agent、Retriever、Tool串了起来。但正因如此新手最容易陷入两个典型误区一是把LLMChain当万能胶水所有逻辑都往里塞结果prompt越写越长、响应越来越慢二是过早追求LangGraph的复杂状态机却连RunnablePassthrough怎么配合ChatPromptTemplate做上下文注入都没搞明白。这本手册不讲“LangChain是干嘛的”这种教科书定义只聚焦一件事从零开始如何用最短路径写出第一个真正能跑通、能调试、能交付的LangChain应用。你会看到LCELLangChain Expression Language如何用一行|符号替代20行胶水代码会理解为什么Retrievers必须和Document的分块策略强绑定也会亲手解决那个高频报错prompt too large for the model背后的真实瓶颈——它往往不是模型限制而是你没对retriever返回的chunk做长度归一化。关键词不是装饰而是你搜索报错时救命的锚点当你搜langchain rag实际要查的是retriever与document loader的耦合方式当你搜prompt engineering提示工程LangChain里真正该调的是ChatPromptTemplate的partial()方法而非手动拼字符串而agentscope和langchain这类对比热词本质是在问“我的业务需要自主决策Agent还是只需条件分支Chain”——答案藏在你是否需要Tool的动态注册与AgentExecutor的循环重试机制里。接下来的内容全部基于真实项目中的最小可运行单元展开每一步都附带我压测过的参数阈值和绕过坑的实操指令。2. LCEL用管道符重构你的思维惯性告别胶水代码LCELLangChain Expression Language不是语法糖它是LangChain对“函数式编程”思想的彻底贯彻。很多教程把它简化为“链式调用”但真实价值在于它强制你把每个处理单元拆解成无状态、可组合、可测试的纯函数。当你写下prompt | model | output_parser你其实在声明一个数据流输入文本→模板填充→大模型推理→结构化解析。这个声明本身就能被序列化、被缓存、被注入调试钩子——而传统写法里你得在model.invoke()前后手动加日志、手动处理异常、手动传递中间变量。2.1 为什么LCEL能解决90%的“prompt太长”问题auto-compaction failed (context overflow: prompt too large for the model)这个报错80%的根源在于开发者用str.format()或f-string硬拼prompt导致历史对话、检索结果、系统指令全部堆在同一个字符串里。LCEL的RunnableMap旧称RunnableParallel提供了一种优雅的解法把不同来源的数据流并行处理再按需注入。看这个真实案例——构建客服问答系统时我们需要同时注入三类信息用户当前问题动态从知识库召回的3个最相关文档片段动态服务协议条款静态但需随版本更新传统写法# 危险所有内容拼成一个超长字符串 full_prompt f 你是一名专业客服请根据以下信息回答用户问题。 【服务协议】{static_terms} 【相关文档】{retrieved_docs[0].page_content} {retrieved_docs[1].page_content} {retrieved_docs[2].page_content} 【用户问题】{user_query} 请用中文回答禁止编造信息。 LCEL写法from langchain_core.runnables import RunnablePassthrough, RunnableMap from langchain_core.prompts import ChatPromptTemplate # 将静态内容提前加载避免每次调用都重复读取 static_context {terms: load_latest_terms()} # 返回dict # 动态部分用户问题 检索结果 dynamic_input { question: RunnablePassthrough(), # 直接透传用户输入 docs: retriever, # 自动调用retriever返回Document列表 } # 构建并行数据流静态动态合并 input_chain RunnableMap( {terms: lambda x: static_context[terms], # 静态内容 question: lambda x: x[question], # 用户问题 docs: lambda x: x[docs]} # 检索结果 ).assign( # assign用于添加新字段而不覆盖 contextlambda x: \n\n.join([d.page_content for d in x[docs][:3]]) # 仅取top3且控制长度 ) # 最终prompt模板注意这里只接收已处理好的字段 prompt ChatPromptTemplate.from_messages([ (system, 你是一名专业客服请严格依据【服务协议】和【相关文档】回答问题。), (human, 【服务协议】{terms}\n\n【相关文档】{context}\n\n【用户问题】{question}) ]) # 完整链路输入→预处理→模板→模型→解析 chain input_chain | prompt | model | StrOutputParser()关键差异在哪长度可控context字段由lambda x: \n\n.join(...)显式生成我们能精确控制retrieved_docs的截取数量[:3]和单个chunk的最大字符数后续章节详解缓存友好static_context只加载一次retriever调用可单独配置缓存调试直观在input_chain后插入.with_config(run_namepreprocess)就能在LangSmith中清晰看到每个字段的值无需在prompt字符串里大海捞针找bug。提示RunnableMap的lambda函数必须返回字典且键名需与ChatPromptTemplate中{key}的占位符完全一致。我曾因把docs写成documents导致模板渲染为空调试耗时40分钟——建议在开发期强制开启prompt.pretty_print()查看最终生成的prompt。2.2 LCEL的隐藏能力动态分支与条件路由当业务逻辑出现“如果用户问价格走报价流程如果问售后走工单流程”时很多人第一反应是写if/else。但LCEL提供了更符合函数式思想的RunnableBranchfrom langchain_core.runnables import RunnableBranch # 定义分类器返回字符串标签 def classify_question(input_dict): question input_dict[question].lower() if any(kw in question for kw in [价格, 多少钱, 贵吗]): return pricing elif any(kw in question for kw in [售后, 维修, 退换]): return after_sales else: return default # 构建分支链路 branch_chain RunnableBranch( (lambda x: classify_question(x) pricing, pricing_chain), # 条件函数 对应链路 (lambda x: classify_question(x) after_sales, after_sales_chain), default_chain # 默认分支 ) # 整合进主链 full_chain {question: RunnablePassthrough()} | branch_chain这里的关键洞察是分支条件本身就是一个独立的Runnable。你可以把classify_question替换成微调的小型分类模型如HuggingFacePipeline甚至接入外部API而整个链路的调用方式不变。这比硬编码if/else更易测试、更易替换、更易监控——在LangSmith中你能清楚看到每次调用走的是哪个分支以及各分支的耗时分布。注意RunnableBranch的条件函数必须返回布尔值且所有分支链路的输入/输出类型需保持一致。我曾因pricing_chain返回dict而default_chain返回str导致下游解析失败解决方案是在分支内统一用| StrOutputParser()收尾。3. Retriever深度实战从“召回文档”到“精准喂养模型”Retriever常被简化为“向量数据库查询接口”但它的真正威力在于它是连接原始数据与大模型认知边界的翻译器。langchain rag搜索热度高恰恰说明多数人卡在了“查到了文档但模型还是答不对”这一步。问题不在向量库而在Retriever如何把Document变成模型能消化的上下文。下面以我部署的金融合规系统为例拆解三个致命细节。3.1 分块策略不是越细越好而是要匹配模型“阅读习惯”金融文档充满表格、条款编号、法律引用若用默认的RecursiveCharacterTextSplitter按\n\n,\n, 分割会导致表格被切成碎片丢失行列关系“第X条”与后续内容分离模型无法理解条款完整性页眉页脚噪声混入正文。正确做法是先做结构化清洗再按语义分块from langchain_text_splitters import HTMLHeaderTextSplitter, RecursiveCharacterTextSplitter # 步骤1用HTMLHeaderTextSplitter识别标题层级金融文档多为HTML/PDF转HTML headers_to_split_on [ (h1, Header 1), (h2, Header 2), (h3, Header 3), ] html_splitter HTMLHeaderTextSplitter(headers_to_split_onheaders_to_split_on) # 步骤2对每个标题块用语义感知的分块器 semantic_splitter RecursiveCharacterTextSplitter( chunk_size512, # 关键不是1024或2048 chunk_overlap64, separators[\n\n, \n, 。, , , , ], # 中文标点优先 keep_separatorTrue, ) # 步骤3组合分块先按标题切再对每个标题块细分 def split_document(doc): # 先按HTML标题切分 header_splits html_splitter.split_text(doc.page_content) # 再对每个标题块做语义分块 final_chunks [] for split in header_splits: chunks semantic_splitter.split_text(split.page_content) for chunk in chunks: # 为每个chunk注入标题上下文避免丢失语义 chunk.metadata.update({header: split.metadata.get(Header 1, )}) final_chunks.extend(chunks) return final_chunks为什么chunk_size512因为我们的主力模型deepseek-v4通过Ollama部署在context_length4096时最佳性能点出现在单次输入2048tokens。而512字符≈256 tokens中文token效率约2字符/token预留空间给prompt模板、系统指令和输出缓冲。实测数据当chunk_size设为1024时模型在整合多个chunk时错误率上升37%因为它开始混淆不同条款的约束条件。经验在split_document后务必打印len(chunk.page_content)和len(chunk.metadata)检查是否有chunk超过chunk_size*1.2分块器允许小幅溢出。我曾发现PDF解析器将页脚“©2024 XXX公司”错误识别为正文导致所有chunk末尾多出20字符噪声——解决方案是在semantic_splitter前插入正则清洗re.sub(r©\d{4}.*$, , text)。3.2 检索增强不只是向量相似度更是多维度置信度融合单纯依赖Chroma的similarity_search在金融场景下召回率不足60%。因为用户提问“流动性风险敞口”文档中可能写的是“现金及现金等价物净流出”向量距离远但语义近。我们采用混合检索Hybrid Retrievalfrom langchain.retrievers import EnsembleRetriever from langchain_community.retrievers import BM25Retriever from langchain_community.vectorstores import Chroma # 向量检索器Chroma vector_retriever vectorstore.as_retriever( search_typesimilarity_score_threshold, search_kwargs{score_threshold: 0.5, k: 5} ) # 关键词检索器BM25对术语匹配敏感 bm25_retriever BM25Retriever.from_documents(all_docs) bm25_retriever.k 5 # 融合检索器加权平均分数 ensemble_retriever EnsembleRetriever( retrievers[vector_retriever, bm25_retriever], weights[0.6, 0.4] # 向量检索权重更高但保留关键词兜底 ) # 使用时直接传入ensemble_retriever chain {context: ensemble_retriever, question: RunnablePassthrough()} | prompt | model关键参数score_threshold0.5不是拍脑袋定的。我们做了A/B测试在1000条真实客服工单上score_threshold从0.3调到0.7召回率从82%降至45%但精确率从33%升至79%。最终选0.5因为业务要求“宁可漏掉1个问题也不能答错1个答案”。weights[0.6, 0.4]则源于错误分析向量检索漏掉的case80%是因术语不一致如“抵押”vs“担保”而BM25能精准捕获但BM25完全无法处理同义替换如“利率”vs“资金成本”必须靠向量补足。提示EnsembleRetriever返回的Document对象其metadata会包含retriever_name字段如vector或bm25你可以在prompt中设计差异化提示“若来源为BM25检索请强调原文措辞若来源为向量检索请进行语义归纳”。3.3 Rerank重排序用小模型为大模型“把关”即使做了混合检索top5结果里仍可能有1-2个低相关性文档。直接喂给大模型会污染其推理过程。我们引入轻量级rerankerBAAI/bge-reranker-base做二次过滤from langchain.retrievers import ContextualCompressionRetriever from langchain.retrievers.document_compressors import CrossEncoderReranker from langchain_community.cross_encoders import HuggingFaceCrossEncoder # 加载reranker模型CPU即可运行1GB显存 model HuggingFaceCrossEncoder(model_nameBAAI/bge-reranker-base) compressor CrossEncoderReranker(modelmodel, top_n3) # 只保留top3 # 压缩检索器先检索再rerank再压缩 compression_retriever ContextualCompressionRetriever( base_compressorcompressor, base_retrieverensemble_retriever ) # 在链路中使用 chain {context: compression_retriever, question: RunnablePassthrough()} | prompt | model效果对比在金融问答测试集上指标仅向量检索混合检索混合rerank平均响应时间1.2s1.5s1.8s答案准确率58%72%89%“我不知道”率24%15%7%多出的0.3秒换来准确率提升17个百分点对客服系统而言意味着每天减少300次人工介入。reranker的top_n3是经过压测的设为5时准确率仅提升1%但延迟增加0.4s设为2时漏掉关键条款的概率上升。注意CrossEncoderReranker需transformers4.35旧版本会报ImportError: cannot import name CrossEncoder。解决方案是升级pip install --upgrade transformers或改用FlashRank更轻量但精度略低。4. Prompt工程从“写提示词”到“设计认知接口”prompt engineering提示工程的热度掩盖了一个事实在LangChain中Prompt不是孤立的字符串而是与模型能力、输入结构、输出解析强耦合的接口协议。prompt has no outputs或prompt outputs failed validation这类报错90%源于Prompt与output_parser的契约断裂。下面用一个真实Agent案例展示如何系统化设计Prompt。4.1 结构化输出用Pydantic强制模型“守规矩”用户需求“分析这份销售合同提取甲方名称、乙方名称、签约日期、总金额、违约金比例”。错误做法用自然语言描述输出格式指望模型自觉遵守。请提取以下信息并用JSON格式输出甲方名称、乙方名称、签约日期、总金额、违约金比例。正确做法用PydanticOutputParser定义Schema让Prompt成为Schema的“自然语言说明书”from langchain.output_parsers import PydanticOutputParser from langchain_core.pydantic_v1 import BaseModel, Field from langchain_core.prompts import PromptTemplate # 1. 定义数据模型强制类型和约束 class ContractInfo(BaseModel): party_a: str Field(description甲方全称需完整准确不可缩写) party_b: str Field(description乙方全称需完整准确不可缩写) sign_date: str Field(description签约日期格式YYYY-MM-DD如2023-05-20) total_amount: float Field(description合同总金额单位万元保留两位小数) penalty_rate: float Field(description违约金比例百分比数值如10.5表示10.5%) # 2. 创建解析器自动生成格式说明 parser PydanticOutputParser(pydantic_objectContractInfo) # 3. 构建Prompt关键将parser的格式说明注入 prompt PromptTemplate( template请从合同文本中提取指定信息。\n{format_instructions}\n\n合同文本\n{contract_text}, input_variables[contract_text], partial_variables{format_instructions: parser.get_format_instructions()} ) # 4. 链路Prompt → 模型 → 解析器自动校验 chain prompt | model | parserparser.get_format_instructions()会生成类似这样的字符串The output should be formatted as a JSON instance that conforms to the JSON schema below. { party_a: {type: string, description: 甲方全称需完整准确不可缩写}, party_b: {type: string, description: 乙方全称需完整准确不可缩写}, sign_date: {type: string, description: 签约日期格式YYYY-MM-DD如2023-05-20}, total_amount: {type: number, description: 合同总金额单位万元保留两位小数}, penalty_rate: {type: number, description: 违约金比例百分比数值如10.5表示10.5%} } Make sure you only output the JSON object, with no additional text or explanations.这个设计解决了三大痛点类型安全total_amount必须是float模型若输出1200万元会被parser拒绝并重试格式强制sign_date必须是YYYY-MM-DD模型若输出2023年5月20日会触发ValidationError错误可追溯当parser失败时报错信息明确指出field sign_date: value is not a valid string而非笼统的prompt outputs failed validation。经验在ContractInfo模型中为每个字段添加Field(defaultNone)可避免模型遗漏字段时报KeyError。但要注意defaultNone会使字段变为可选需在业务逻辑中二次校验必填项。4.2 Agent的Prompt设计给模型装上“决策操作系统”langchain agent实战的难点不在Tool编写而在如何让模型理解“何时调用哪个Tool”、“如何组合Tool结果”、“失败后如何降级”。这需要Prompt承担“操作系统”的角色。以一个电商客服Agent为例from langchain.agents import create_tool_calling_agent, AgentExecutor from langchain.tools import tool # 定义Tool模拟调用订单系统 tool def get_order_status(order_id: str) - str: 根据订单ID查询订单状态返回JSON格式 return {status: shipped, tracking_number: SF123456789} # 构建Agent Prompt核心 agent_prompt ChatPromptTemplate.from_messages([ (system, 你是一名专业电商客服负责解答用户关于订单、物流、售后的问题。\n 【工具使用规则】\n - 当用户提及订单号含SF、YT、ZTO等快递单号前缀时必须调用get_order_status工具。\n - 当用户询问发货了吗、到哪了时若未提供订单号需先追问请提供您的订单号或快递单号。\n - 工具调用失败时回复系统暂时繁忙请稍后再试禁止编造信息。\n 【输出规则】\n - 所有回复必须用中文口语化带表情符号。\n - 禁止暴露工具名称、参数、JSON结构等技术细节。 ), (placeholder, {chat_history}), # 历史消息占位符 (human, {input}), # 用户当前输入 (placeholder, {agent_scratchpad}), # Agent内部思考占位符 ]) # 创建Agent agent create_tool_calling_agent( llmmodel, tools[get_order_status], promptagent_prompt ) agent_executor AgentExecutor(agentagent, tools[get_order_status], verboseTrue)这个Prompt的设计哲学是用自然语言穷举边界条件而非依赖模型“脑补”。我们刻意写了三条规则必须调用消除模型犹豫避免该调不调需先追问处理缺失关键参数的case防止工具调用失败系统暂时繁忙定义降级话术避免暴露系统脆弱性。实测中未加【工具使用规则】时模型在20%的订单号场景下选择“我不知道”加了规则后降至0.3%。{agent_scratchpad}占位符是LangChain Agent的魔法所在——它会自动填入模型的思考链Thought、工具调用Action、工具结果Observation形成完整的推理轨迹这是调试Agent行为的唯一途径。提示在AgentExecutor中设置handle_parsing_errorsTrue可捕获Action解析失败如模型输出{action: get_order_status, action_input: SF123}但缺少}避免整个Agent崩溃。错误信息会包含原始agent_scratchpad内容方便定位是Prompt引导不足还是模型能力缺陷。5. 生产级避坑指南从本地Demo到稳定服务的12个生死线写完第一个chain.invoke()得到预期结果只是万里长征第一步。真正的挑战在部署后context overflow报错频发、retriever响应超时、Agent无限循环、checkpoint-blob存储失败……这些不是边缘case而是生产环境的日常。以下是我在三套上线系统中总结的12个关键避坑点按致命程度排序。5.1 “prompt too large”报错的根因诊断树当出现auto-compaction failed (context overflow: prompt too large for the model)不要急着删减prompt。按此顺序排查排查步骤检查方法修复方案我的踩坑实例1. 检查retriever返回的Document数量与长度print([len(d.page_content) for d in retriever.invoke(query)])设置retriever.k3并在RunnableMap中用[:3]截断曾因k10且未截断单次调用注入32KB文本远超模型4KB上下文2. 检查prompt模板的静态内容print(prompt.format_prompt(questiontest).to_string())将长静态文本如服务协议移至RunnableMap外用lambda加载协议文本2.1MB每次调用都加载内存暴涨3. 检查历史消息chat_history累积print(len(chat_history))实现chat_history的滑动窗口只保留最近5轮未清理历史100轮后chat_history达8MBOOM4. 检查模型自身的context_length限制print(model.model_name)→ 查官网文档切换为context_length更大的模型如qwen2-72b误用gpt-3.5-turbo-16k的API但Ollama部署的是qwen2-7b仅4K关键技巧在chain中插入.with_config(run_namedebug_prompt)然后在LangSmith中查看debug_prompt节点的inputs和outputs能直接看到最终生成的prompt字符串及其长度。这是最高效的定位手段。5.2 Retriever性能优化从秒级到毫秒级的跨越langchain使用chroma常见问题本地开发快一上服务器就超时。根本原因在于Chroma默认使用hnswlib其ef_construction和M参数未针对生产负载调优# 错误使用默认参数适合小数据集 vectorstore Chroma.from_documents(docs, embedding_model) # 正确生产级参数基于100万文档压测 vectorstore Chroma( persist_directory./chroma_db, embedding_functionembedding_model, collection_metadata{ hnsw:space: cosine, # 距离度量 hnsw:construction_ef: 128, # 构建时邻居数越大越准但越慢 hnsw:M: 64, # 每层最大连接数越大越准但内存越高 hnsw:search_ef: 64, # 搜索时邻居数越大越准但越慢 } )参数选择逻辑hnsw:construction_ef128构建索引时更精细牺牲10%构建时间换取30%召回率提升hnsw:M64平衡内存约1.2GB与精度M32时内存减半但召回率降15%hnsw:search_ef64线上搜索时ef64比ef32响应快120ms且准确率无损。经验在vectorstore初始化后执行vectorstore._collection.count()确认文档数并用timeit测试retriever.invoke(test)耗时。目标P95延迟300ms。若超时优先调大search_ef其次检查嵌入模型是否在GPU上运行devicecuda。5.3 Agent循环陷阱如何让AI“知进退”langchain agent实战中最隐蔽的坑是Agent无限循环。例如用户问“帮我订一张去北京的机票”Agent反复调用search_flights工具却因缺少日期参数一直失败。LangChain的AgentExecutor默认max_iterations15但15次循环足以拖垮服务。根治方案在Prompt中定义“失败熔断”规则并用handle_parsing_errors捕获# 在system prompt中加入 【失败熔断规则】\n - 若同一工具连续调用3次均失败返回error立即停止调用回复暂无法处理该请求请联系人工客服。\n - 若工具返回未找到结果且用户未提供必要参数如日期、城市需明确追问具体信息。 # 创建AgentExecutor时启用错误处理 agent_executor AgentExecutor( agentagent, tools[search_flights, book_hotel], verboseTrue, max_iterations5, # 降低默认值 handle_parsing_errors请重新表述您的请求确保包含必要信息如出发地、目的地、日期 # 自定义错误话术 )更进一步可自定义AgentExecutor的intermediate_steps回调实时监控工具调用序列def on_tool_end(retry_count0): def callback(step): # step[0]是tool调用step[1]是结果 if error in str(step[1]).lower() and retry_count 2: # 记录错误准备重试 print(fTool {step[0].tool} failed, retrying...) elif error in str(step[1]).lower(): # 达到重试上限强制退出 raise ValueError(Tool failed after retries) return callback agent_executor AgentExecutor(..., callbacks[on_tool_end()])我的教训某次上线Agent因天气API临时不可用连续15次调用失败导致服务雪崩。现在所有Agent都强制max_iterations5并在Prometheus中监控agent_loop_count指标超过阈值自动告警。5.4 Checkpoint持久化避免Agent“失忆”的终极方案langchain checkpoint-blob保存类型exttype报错本质是LangChain的Checkpoint机制与存储后端不兼容。langgraph和langchain的区别中LangGraph的checkpointer更成熟但LangChain的BaseCheckpointSaver需手动适配。生产环境必须用PostgresSaver支持事务、并发、备份from langgraph.checkpoints.postgres import PostgresSaver import psycopg2 # 初始化PostgreSQL连接池 conn psycopg2.connect( hostlocalhost, databaselangchain_db, userlangchain, passwordsecret ) # 创建checkpointer自动建表 checkpointer PostgresSaver(conn) checkpointer.setup() # 创建langgraph_checkpoints表 # 在AgentExecutor中启用 agent_executor AgentExecutor( agentagent, tools[...], checkpointercheckpointer, # 关键启用持久化 interrupt_before[action], # 可在action前中断供人工审核 )exttype报错通常因PostgresSaver未正确初始化或表结构不匹配。解决方案确保psycopg22.9.0执行checkpointer.setup()创建表若已存在旧表用checkpointer.upgrade_schema()迁移。关键价值启用checkpointer后Agent可跨请求保持状态。用户说“查一下我的订单”Agent记住user_id用户接着问“那物流呢”Agent自动关联上一单。这不再是“无状态API”而是真正的“有记忆Agent”。6. 从LangChain到LangGraph何时该升级你的架构langchain和langgraph的区别不是版本迭代而是范式跃迁。LangChain适合线性流程Input → Process → OutputLangGraph适合状态驱动流程State → Action → State → ...。当你的业务出现以下信号就是升级的临界点需要人工干预节点如“合同审核Agent”中AI初审后需法务人工确认再进入盖章环节存在循环依赖如“数据分析Agent”需根据初步结果决定是否调用run_sql或fetch_data结果又影响下一步多角色协同如“投研报告Agent”中研究员、风控、合规三方需按规则依次评审。LangGraph的核心是StateGraph它用add_node定义处理单元用add_edge定义流转规则from langgraph.graph import StateGraph, END from typing import TypedDict, Annotated, List # 1. 定义状态所有节点共享的数据容器 class ResearchState(TypedDict): topic: str draft: str sources: List[str] human_feedback: str approved: bool # 2. 定义节点纯函数接收state返回state更新 def research_node(state: ResearchState) - ResearchState: # 调用retriever和model生成初稿 draft chain.invoke({topic: state[topic]}) return {draft: draft, sources: [doc1, doc2]} def review_node(state: ResearchState) - ResearchState