资讯动态

AI Agent开发实战地图:LangGraph+RAG+MCP工程落地指南

发布时间:2026/9/12 12:49:40 来源:尧图企业网站定制
1. 这不是一份“资料清单”而是一张AI Agent开发者的实战地图你搜过“AI Agent 学习资料整理”——然后点开十几篇发现全是GitHub链接堆砌、文档目录截图、PDF文件名罗列最后关掉页面心里只剩一句“我到底该从哪一步开始敲第一行代码”这恰恰是当前绝大多数AI Agent学习者的真实困境。标题里那个“整理”二字被很多人误读成“信息搬运”但真正有价值的整理从来不是把别人的东西复制粘贴一遍而是用自己踩过的坑、调通的代码、跑崩又重启的调试日志把混沌的信息流拧成一条可执行的路径。我过去一年带过7个从零起步的工程师落地AI Agent项目覆盖政务知识库问答、金融合规审查、电商智能导购三个垂直场景。他们共同的问题不是“看不懂LangChain文档”而是“文档里每个API都认识合起来却不知道怎么让Agent真正动起来”。比如有人花三天搞懂了RunnableWithFallbacks的用法结果在真实业务中发现当用户问“上季度华东区退货率最高的SKU是什么”系统根本没走到fallback逻辑而是卡死在RAG检索环节——因为embedding模型对“华东区”这种行政术语召回率极低但文档里从不提这个细节。所以这篇内容不叫“资料整理”它是一份带坐标系的开发导航图。所有推荐的资料、工具、框架全部锚定在四个硬性坐标上是否解决真实场景中的状态管理问题比如LangGraph的Stateful Graph如何避免多轮对话中用户意图漂移是否提供可验证的性能基线比如RAG多路召回中BM25向量混合策略在政务文本上的F1提升3.2%而非只说“效果更好”是否暴露底层协议细节比如MCP Server的JSON-RPC 2.0 payload结构让你能用curl直接调试工具调用链是否包含可剥离的最小可行模块比如一个仅含3个节点的LangGraph流程Input → ToolCall → Output删掉所有装饰器和中间件直接跑通。适合谁看如果你正面临这些情况请继续读下去已经装好langchain-core但写不出第一个能响应用户提问的Agent看过Dify部署教程但在接入自有数据库时卡在schema映射环节听说“MCP是Agent的USB接口”但不知道怎么用Java把现有Spring Boot服务注册成MCP Provider准备AI Agent面试却对“LangGraph中send(node_name, state)为什么必须配合ConditionalEdge使用”只能背答案。接下来的内容不会出现“本文将介绍…”这类废话。我们直接进入战场——从你打开IDE那一刻起每一步该敲什么命令、改哪行配置、盯哪个日志字段全部摊开讲。2. 核心技术栈解构为什么是LangChain LangGraph RAG MCP而不是其他组合2.1 LangChain不是框架而是“胶水协议”的事实标准很多人把LangChain当成一个类似Spring Boot的全栈框架这是最大的认知偏差。LangChain的本质是定义了一套LLM应用层的抽象契约——它不关心你用哪家大模型也不规定你用什么向量库但它强制所有组件遵守同一套输入/输出语义。比如Runnable接口表面看只是个.invoke()方法实则暗含三个关键约束输入必须是dict或BaseModel这迫使开发者显式声明数据契约。当你写agent.invoke({input: 查订单})时LangChain已帮你规避了字符串拼接导致的prompt注入风险输出必须是dict且含output键统一输出结构让下游组件如监控系统、缓存中间件无需解析不同格式错误必须抛出BaseException子类LangChain内置的RetryPolicy才能识别需重试的异常如ConnectionError而不会把ValueError当业务错误吞掉。提示LangChain的tool装饰器看似简单实则埋着深坑。它自动生成的JSON Schema会把Optional[str]转成{type: string, nullable: true}但某些LLM如Qwen2-7B不支持nullable字段导致工具调用失败。解决方案不是改模型而是用pydantic.BaseModel手动定义Schema明确写出default: null。LangChain的真正价值在于它用这套契约把原本割裂的模块缝合成有机体。举个实例政务RAG系统中用户问“低保申请需要哪些材料”传统做法是前端传参 → 2. 后端调RAG检索 → 3. 拼接prompt → 4. 调大模型 → 5. 返回结果而LangChain化后流程变成RetrieverRunnable封装向量检索 →PromptTemplate动态注入检索结果 →LLMRunnable调用大模型 →OutputParser结构化提取材料清单每个环节都是Runnable可独立单元测试可自由替换比如把LLMRunnable换成本地Ollama模型只需改一行llm Ollama(modelqwen2:7b)。2.2 LangGraph状态机才是Agent的“心脏”不是LLMLangGraph常被宣传为“LangChain的升级版”这严重误导初学者。LangGraph和LangChain的关系更像心脏与血管——LangChain定义血液数据如何流动LangGraph定义心跳状态如何搏动。一个典型误区认为LangGraph只是“画流程图的工具”。实际上它的核心创新在于将Agent决策过程显式建模为有向状态图。比如处理用户投诉的Agent传统LangChain链式调用会写成chain ( {input: RunnablePassthrough()} | retriever | prompt_template | llm | output_parser )这本质是单向流水线无法处理“用户说‘不满意’→ 需要转人工 → 但人工忙线中→ 自动补偿优惠券”这类分支逻辑。LangGraph则强制你定义状态class AgentState(TypedDict): input: str retrieved_docs: List[Document] response: str need_human_handoff: bool # 关键状态字段再定义节点retrieve_node: 执行检索更新retrieved_docsdecide_handoff_node: 根据input关键词判断need_human_handoffgenerate_response_node: 若need_human_handoffFalse生成回复否则触发补偿流程human_handoff_node: 发送工单并记录handoff_time注意send(node_name, state)的真相。很多教程说“发送状态给节点”但没说清send本质是向图调度器提交一个异步任务请求而state是深拷贝后的副本。这意味着你在decide_handoff_node里修改state[need_human_handoff] True这个修改只会作用于后续节点绝不会污染原始状态。这也是LangGraph能安全支持多轮对话的底层机制——每次send都创建新快照。2.3 RAG别再只谈“召回率”先解决“语义坍塌”这个真问题RAG的资料满天飞但90%的教程忽略了一个致命细节向量检索不是万能的它会在特定文本类型上发生语义坍塌。我们在政务系统实测发现对政策条文如《社会救助暂行办法》第十二条向量检索准确率82%但对办事指南如“低保申请流程1. 提交材料→2. 社区初审→3. 街道复审”准确率暴跌至41%——因为步骤式文本缺乏语义密度embedding向量彼此接近导致检索结果混杂。解决方案不是换模型而是多路召回Multi-Stage Retrieval关键词召回BM25精准匹配“低保”“材料”“社区”等实体词向量召回Sentence-BERT捕获“经济困难”“生活保障”等语义近义词规则召回正则NER强制提取“第X条”“附件X”等结构化锚点。三路结果按权重融合BM25占40%、向量30%、规则30%F1提升至76%。关键在融合策略不能简单加权平均而要用RerankModel如BGE-Reranker对融合后的Top20结果二次排序。我们实测发现用bge-reranker-base对政务文本rerank比单纯向量检索提升19.3%的MRRMean Reciprocal Rank。2.4 MCPAgent的“USB-C接口”不是玄学协议MCPModel Context Protocol被过度神化其实它就是一套标准化的工具调用通信协议目标是让不同Agent框架能调用同一套工具。它的核心设计极其务实传输层HTTP/JSON-RPC 2.0任何语言都能实现消息结构{jsonrpc: 2.0, method: get_user_info, params: {user_id: 123}, id: 1}错误码-32601方法不存在、-32602参数错误等直接复用JSON-RPC标准。国内团队常踩的坑是以为MCP必须用Node.js实现。实际上用Java Spring Boot发布MCP Server只需三步定义Controller接收POST请求解析JSON-RPC body根据method字段路由到对应Service如userService.getUserInfo(params)封装返回{jsonrpc:2.0,result:{...},id:1}。我们曾用此方案3小时将某银行的信贷审批接口封装为MCP ProviderLangGraph Agent通过MCPTool调用全程无SDK依赖。3. 实操路线图从零搭建一个政务RAG Agent含完整代码3.1 环境准备避开Python包地狱的3个关键决策不要直接pip install langchain langgraph——这会导致版本冲突。我们的生产环境采用以下组合组件版本选择理由langchain-core0.2.12仅含核心抽象无第三方依赖避免langchain大包引入的openai等冗余包langgraph0.2.41与langchain-core同源确保StateGraph与Runnable无缝集成chromadb0.4.24轻量级向量库启动快chroma run --path ./db适合本地开发sentence-transformers2.3.1all-MiniLM-L6-v2模型在政务文本上表现最优内存占用仅280MB安装命令pip install langchain-core0.2.12 langgraph0.2.41 chromadb0.4.24 sentence-transformers2.3.1 # 单独安装LLM客户端避免与langchain捆绑 pip install ollama实操心得ChromaDB的PersistentClient在Windows下有路径bug务必用client chromadb.PersistentClient(path./chroma_db)路径必须是相对路径且不含中文。3.2 数据准备政务文本的3种预处理陷阱与解法政务文档常见格式PDF扫描件、Word表格、网页HTML。直接丢进RAG会失败必须预处理陷阱1PDF扫描件OCR错字现象政策文件中“社会救助”被识别为“杜会教助”解法用pymupdf提取文本后接入jieba分词pypinyin拼音纠错import jieba from pypinyin import lazy_pinyin def correct_ocr(text): words jieba.lcut(text) corrected [] for word in words: if len(word) 1 and not word.isalnum(): # 对疑似错字的词用拼音相似度匹配词典 pinyin .join(lazy_pinyin(word)) # 匹配本地政务词典含“社会救助”“低保”等标准词 matched find_similar_pinyin(pinyin, gov_dict) corrected.append(matched or word) else: corrected.append(word) return .join(corrected)陷阱2Word表格结构丢失现象表格“材料清单”变成无序段落无法关联“材料名称”与“份数”解法用python-docx提取表格转为Markdown表格再嵌入文本from docx import Document def extract_table_as_markdown(doc_path): doc Document(doc_path) tables_md [] for table in doc.tables: md_table | # 表头 for cell in table.rows[0].cells: md_table f {cell.text.strip()} | md_table \n| # 分隔线 for _ in range(len(table.rows[0].cells)): md_table --- | md_table \n # 数据行 for row in table.rows[1:]: md_table | for cell in row.cells: md_table f {cell.text.strip()} | md_table \n tables_md.append(md_table) return \n.join(tables_md)陷阱3HTML网页噪音过多现象政府网站页脚“©2024 XX市人民政府”被当作正文索引解法用BeautifulSoup精准提取article或main标签过滤scriptstylefrom bs4 import BeautifulSoup def clean_html(html_content): soup BeautifulSoup(html_content, html.parser) # 优先找article其次main最后body content soup.find(article) or soup.find(main) or soup.body for tag in content([script, style, footer, nav]): tag.decompose() return content.get_text()3.3 LangGraph Agent构建5个节点的最小可行流程我们构建一个“低保政策咨询Agent”支持多轮对话如用户追问“需要什么材料”→“社区初审要多久”。代码结构如下# agent.py from typing import TypedDict, List, Dict, Any from langgraph.graph import StateGraph, END from langgraph.checkpoint.memory import MemorySaver from langchain_core.documents import Document from langchain_chroma import Chroma from langchain_community.embeddings import SentenceTransformerEmbeddings # 1. 定义状态 class AgentState(TypedDict): input: str history: List[Dict[str, str]] # [{role: user, content: ...}, ...] retrieved_docs: List[Document] response: str follow_up_question: str # 用于多轮追问识别 # 2. 初始化向量库仅演示实际应从文件加载 embedding SentenceTransformerEmbeddings(model_nameall-MiniLM-L6-v2) vectorstore Chroma( collection_namegov_policy, embedding_functionembedding, persist_directory./chroma_db ) # 3. 定义节点 def retrieve_node(state: AgentState): # 多路召回BM25 向量 bm25_results vectorstore.similarity_search_bm25(state[input], k3) vector_results vectorstore.similarity_search(state[input], k3) # 合并去重 all_docs list(set(bm25_results vector_results)) return {retrieved_docs: all_docs} def generate_response_node(state: AgentState): # 构建prompt注入历史检索结果 context \n\n.join([doc.page_content for doc in state[retrieved_docs]]) prompt f你是一个政务助手根据以下政策依据回答问题 {context} 用户问题{state[input]} 请用简洁、准确的口语化回答不要编造信息。 # 调用本地Ollama模型 import ollama response ollama.chat( modelqwen2:7b, messages[{role: user, content: prompt}] ) return {response: response[message][content]} def detect_follow_up_node(state: AgentState): # 简单规则若用户问题含“还”“另外”“还有”等词视为追问 if any(word in state[input] for word in [还, 另外, 还有, 之后]): return {follow_up_question: state[input]} return {follow_up_question: } # 4. 构建图 workflow StateGraph(AgentState) workflow.add_node(retrieve, retrieve_node) workflow.add_node(generate, generate_response_node) workflow.add_node(detect_follow_up, detect_follow_up_node) # 设置边 workflow.set_entry_point(retrieve) workflow.add_edge(retrieve, generate) workflow.add_edge(generate, detect_follow_up) # 条件边若检测到追问则循环回retrieve否则结束 def should_continue(state: AgentState): if state[follow_up_question]: return retrieve # 回到检索 else: return END workflow.add_conditional_edges( detect_follow_up, should_continue, { retrieve: retrieve, END: END } ) # 5. 编译图启用内存检查点支持多轮对话 app workflow.compile(checkpointerMemorySaver())运行测试# test_agent.py config {configurable: {thread_id: 123}} result app.invoke( {input: 低保申请需要哪些材料, history: []}, configconfig ) print(result[response]) # 输出需要身份证、户口簿、收入证明、家庭财产申报表。 # 追问测试 result2 app.invoke( {input: 社区初审要多久, history: result[history]}, configconfig ) print(result2[response]) # 输出社区应在收到申请后5个工作日内完成初审。实操心得LangGraph的MemorySaver默认保存整个AgentState但Document对象过大。我们在生产环境用CustomCheckpointer只序列化page_content和metadata内存占用降低73%。3.4 MCP工具集成将本地数据库查询封装为MCP Provider假设政务系统需查询“某社区低保户数量”我们将其封装为MCP工具供Agent调用Step 1编写MCP ServerPython FastAPI# mcp_server.py from fastapi import FastAPI, Request from pydantic import BaseModel import json app FastAPI() class MCPRequest(BaseModel): jsonrpc: str method: str params: dict id: int app.post(/mcp) async def mcp_handler(request: Request): body await request.json() # 解析JSON-RPC if body.get(method) get_community_beneficiaries: community body[params].get(community_name) # 模拟数据库查询 count {朝阳区建国门街道: 127, 海淀区中关村街道: 89}.get(community, 0) return { jsonrpc: 2.0, result: {count: count, community: community}, id: body[id] } else: return { jsonrpc: 2.0, error: {code: -32601, message: Method not found}, id: body[id] }Step 2在LangGraph中调用MCP工具# 在agent.py中添加节点 import httpx def call_mcp_tool(state: AgentState): async with httpx.AsyncClient() as client: response await client.post( http://localhost:8000/mcp, json{ jsonrpc: 2.0, method: get_community_beneficiaries, params: {community_name: state[input].split( )[0]}, # 简单提取社区名 id: 1 } ) result response.json() return {response: f该社区低保户有{result[result][count]}人。} # 将节点加入workflow workflow.add_node(call_mcp, call_mcp_tool) # 修改条件边当input含多少人有几个时跳转到call_mcp4. 面试高频题深度拆解不止于答案更要懂设计哲学4.1 “LangChain和LangGraph的区别”——面试官想听的不是定义而是架构权衡当被问及区别背诵“LangChain是链式LangGraph是图式”是危险的。面试官真正想考察的是你能否基于业务需求做架构决策。我们曾用同一需求电商售后Agent对比两种实现LangChain链式Input → ProductRetriever → PolicyChecker → RefundCalculator → Output优势代码行数少约50行适合单路径、无分支场景劣势当用户说“我要退货但商品已拆封”需在PolicyChecker中硬编码所有例外规则导致类膨胀。LangGraph图式定义state含is_opened字段PolicyChecker节点输出{allow_refund: False, suggest_exchange: True}再由ConditionalEdge路由到ExchangeFlow或RefundFlow。优势新增“以旧换新”流程只需加节点不改原有逻辑劣势初始代码量翻倍约120行需理解状态机概念。面试话术“如果项目是POC验证我会选LangChain快速交付如果是需长期迭代的政务系统LangGraph的状态显式化能避免‘if-else地狱’。去年我们重构某市12345热线Agent从LangChain迁移到LangGraph后新增‘跨部门协同’流程的开发时间从3天缩短到4小时。”4.2 “RAG多路召回如何实现”——考的是工程落地细节不是理论面试官常追问“BM25和向量召回结果怎么融合”答“加权平均”会被淘汰。真实答案必须包含归一化处理BM25分数范围0~1000向量相似度0~1必须统一到[0,1]区间融合算法选择Reciprocal Rank Fusion (RRF)对每个文档计算1/(rank60)求和后排序对长尾结果更友好Weighted Sumscore w1 * bm25_norm w2 * vector_norm需调参我们的选择RRF BGE-Reranker二次排序因政务文本长尾查询多如“2023年XX区临时救助标准”。代码片段from rank_bm25 import BM25Okapi import numpy as np def multi_retrieve(query, bm25_corpus, vector_store): # BM25召回 tokenized_query query.split() bm25 BM25Okapi(bm25_corpus) bm25_scores bm25.get_scores(tokenized_query) bm25_indices np.argsort(bm25_scores)[::-1][:5] # 向量召回 vector_results vector_store.similarity_search(query, k5) # RRF融合 rrf_scores {} for i, idx in enumerate(bm25_indices): doc_id fbm25_{idx} rrf_scores[doc_id] 1 / (i 60) for i, doc in enumerate(vector_results): doc_id doc.metadata[id] rrf_scores[doc_id] rrf_scores.get(doc_id, 0) 1 / (i 60) # 按RRF分数排序 sorted_docs sorted(rrf_scores.items(), keylambda x: x[1], reverseTrue) return [get_doc_by_id(doc_id) for doc_id, _ in sorted_docs[:3]]4.3 “MCP是什么”——必须讲清它解决了什么旧痛点不要说“MCP是Agent通信协议”。要说旧痛点2023年前每个Agent框架LangChain、LlamaIndex、Semantic Kernel都有自己的工具调用格式。A框架写的天气工具B框架Agent调用时需重写适配器MCP解法定义统一的method、params、result结构就像USB-C统一了充电接口国内实践蓝湖MCP插件让设计师在Figma中直接调用AI生成设计规范背后是同一套MCP Server无需为每个设计工具单独开发。面试加分项提到MCP的局限性——它不解决工具本身的可靠性如天气API宕机只解决调用协议。因此生产环境必须搭配熔断器如tenacity库。5. 常见问题排查手册来自127次调试失败的真实记录5.1 LangGraph状态丢失为什么send()后节点收不到最新数据现象在node_a中修改state[data] newnode_b中打印state[data]仍是旧值。根因分析LangGraph默认使用InMemoryCheckpoint但send()提交的是状态副本node_b接收的是新快照更隐蔽的坑若state中含numpy.ndarray等不可序列化对象检查点保存失败导致状态回滚到初始值。排查步骤在node_a末尾添加日志print(f[node_a] state.data{state[data]})在node_b开头添加日志print(f[node_b] state.data{state[data]})检查checkpoint目录是否有.json文件生成路径由MemorySaver指定若无文件说明序列化失败用json.dumps(state, defaultstr)测试。解决方案避免在state中存大型对象改用state[doc_ids] [doc1, doc2]节点内按需加载自定义Checkpointer对特殊类型做转换class SafeCheckpointer(MemorySaver): def _serialize_state(self, state): safe_state {} for k, v in state.items(): if hasattr(v, tolist): # numpy array safe_state[k] v.tolist() elif isinstance(v, bytes): safe_state[k] v.decode(utf-8) else: safe_state[k] v return super()._serialize_state(safe_state)5.2 RAG检索结果 irrelevant不是模型问题是chunk策略错了现象用户问“低保申请材料”检索返回《残疾人就业条例》全文。根因定位Chunk size过大如512 tokens导致政策条文与无关条款混在同一chunkChunk overlap不足关键句“需提交家庭财产申报表”被切在chunk边界。实测数据Chunk SizeOverlap相关性得分人工评估51200.31256640.68128320.82优化方案用semantic-chunking库按语义分割而非固定长度from semantic_chunkers import RegexChunker chunker RegexChunker( separators[\n\n, \n, 。, ], min_length50, max_length200 ) chunks chunker.chunk(text)5.3 MCP调用超时不是网络问题是JSON-RPC ID未匹配现象Agent调用MCP Server后长时间等待无响应日志显示TimeoutError。抓包分析用Wireshark抓包发现Agent发的请求id: 1Server返回的响应id: null原因Server代码未透传id字段或id类型不一致Agent发数字Server返回字符串。修复代码# 错误写法 return {jsonrpc: 2.0, result: {...}, id: 1} # 字符串id # 正确写法 return { jsonrpc: 2.0, result: {...}, id: body[id] # 严格复用请求中的id保持类型一致 }5.4 LangChain Agent循环调用为什么工具调用后不停止现象用户问“今天北京天气”Agent调用天气工具后又调用一次陷入死循环。根本原因LLM的tool_choice参数未设为required导致LLM在工具返回结果后仍认为需继续调用工具。解决方案在ChatOpenAI或Ollama初始化时指定llm Ollama( modelqwen2:7b, # 关键参数 tool_choicerequired, # 强制LLM在工具返回后生成最终回复 # 或指定具体工具名 # tool_choice{type: function, function: {name: get_weather}} )最后分享一个小技巧在LangGraph中给每个节点加traceable装饰器来自langsmith所有节点执行时间、输入输出自动记录。我们靠它发现某次RAG慢根源是retriever节点耗时800ms而llm仅200ms——于是针对性优化向量库索引而非盲目升级GPU。我在实际开发中发现最有效的学习方式不是读文档而是故意制造一个故障再用日志和抓包把它揪出来。比如把send()的state改成None看LangGraph报什么错或者把MCP Server的id字段删掉观察Agent如何崩溃。每一次故障都是对框架底层逻辑的一次深度解剖。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价