1. 从 RAG 到业务闭环,企业 Agent 卡在哪一步
企业级 LLM Agent 落地时,最常见的起点是一条 RAG 流水线:用户提问、向量检索、拼接 Prompt、模型生成答案。这条链路能跑通 Demo,但一旦进入真实业务,问题立刻暴露。比如运营同学问“帮我查一下上周华东区退货率异常的原因,并给对应供应商发一封说明邮件”,传统 RAG 只能检索到几段文档,既不会拆解任务,也不会调用工单系统,更不会在检索结果不相关时自我修正。
我试过把这类需求硬塞进单轮 Prompt,结果就是模型一本正经地编造数据来源。根因在于:传统 RAG 是一个线性函数,输入到输出一次完成;而真实业务需要的是一个状态机,具备记忆、路由、重试和行动能力。LangGraph 提供了状态机编排能力,MCP 协议标准化了外部工具接入方式,两者结合才能把“检索命中”推进到“业务动作触发”。
这篇文章面向正在做企业级 Agent 的工程师,交付三样东西:可复制的 LangGraph 节点与状态定义骨架、MCP 服务接入配置片段、从检索命中到业务动作触发的端到端验证步骤。读完后你可以搭出一个可运行的闭环 Demo,而不是停留在概念层。
2. TaoToken 前置:模型接入与 Key 准备
在写 LangGraph 节点之前,先把模型调用通道准备好。企业级 Agent 对模型的要求是稳定、可切换、便于统一计费。TaoToken 提供 OpenAI 兼容接口,可以直接替换 LangChain 里的 ChatOpenAI 基址,不需要改业务代码。
你需要先拿到 API Key。访问控制台创建密钥:
https://taotoken.net/console?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content=console创建完成后,在 API Keys 页面复制密钥:
https://taotoken.net/api-keys?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content=api-keys接入文档里有完整的兼容说明和参数列表,建议先扫一遍:
https://taotoken.net/doc?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content=docAPI 基址统一使用:
https://taotoken.net/api注意这里不要加 UTM 参数,否则部分 SDK 会把查询串拼进请求路径导致 404。把 Key 写进环境变量,不要硬编码:
export TAOTOKEN_API_KEY="sk-你的密钥" export TAOTOKEN_BASE_URL="https://taotoken.net/api"如果你后续要做长期编码类 Agent,比如自动改代码、跑测试、提交 PR,可以了解 Coding Plan,它更适合高频、长会话的 Agent 场景:
https://taotoken.net/coding-plan?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content=coding-plan模型对话调试入口在这里,验证模型是否通的时候直接用:
https://taotoken.net/chat?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content=model-chat3. 可复制配置:LangGraph 状态机与 MCP 接入
3.1 依赖安装与模型初始化
先装依赖。LangGraph 负责编排,langchain-mcp-adapters 负责把 MCP 工具转成 LangChain 工具:
pip install langgraph langchain-openai langchain-community \ langchain-mcp-adapters httpx redis模型初始化时把 base_url 指向 TaoToken,其余参数和官方 SDK 一致:
# config/llm.py import os from langchain_openai import ChatOpenAI, OpenAIEmbeddings llm = ChatOpenAI( model="gpt-4o-mini", temperature=0, api_key=os.environ["TAOTOKEN_API_KEY"], base_url=os.environ["TAOTOKEN_BASE_URL"], ) embeddings = OpenAIEmbeddings( model="text-embedding-3-small", api_key=os.environ["TAOTOKEN_API_KEY"], base_url=os.environ["TAOTOKEN_BASE_URL"], )3.2 AgentState 状态定义
状态是整个 Agent 的记忆载体。企业场景里必须显式记录重试次数和评分结果,否则容易出现无限循环:
# agents/state.py from typing import TypedDict, List, Literal from langchain_core.documents import Document class AgentState(TypedDict): question: str # 用户原始问题 route: str # 路由决策: vector | graph | web | direct documents: List[Document] # 检索到的文档 grade_results: List[str] # 文档相关性评分 rewrite_count: int # 查询改写次数,防死循环 generation: str # 最终答案 action_result: str # 业务动作执行结果3.3 核心节点:路由、评分、改写、生成
路由节点负责判断问题该走向量检索、图检索还是直接回答。用结构化输出约束模型,避免解析自然语言:
# agents/nodes.py from pydantic import BaseModel from typing import Literal from config.llm import llm from agents.state import AgentState class RouteDecision(BaseModel): route: Literal["vector", "graph", "web", "direct"] reasoning: str router_llm = llm.with_structured_output(RouteDecision) def router_node(state: AgentState) -> AgentState: decision = router_llm.invoke( f"将用户问题分类为 vector/graph/web/direct:\n{state['question']}" ) return {**state, "route": decision.route}评分节点逐个判断文档是否相关,这是自我修正闭环的触发点:
class GradeDoc(BaseModel): score: Literal["relevant", "irrelevant"] grader_llm = llm.with_structured_output(GradeDoc) def grader_node(state: AgentState) -> AgentState: grades = [] for doc in state["documents"]: result = grader_llm.invoke( f"问题:{state['question']}\n文档:{doc.page_content}\n是否相关?" ) grades.append(result.score) return {**state, "grade_results": grades}改写节点在检索失败时把问题改得更具体,同时累加计数:
def rewriter_node(state: AgentState) -> AgentState: new_query = llm.invoke( f"将以下问题改写为更适合检索的形式,关键词更明确:\n{state['question']}" ).content return { **state, "question": new_query, "rewrite_count": state.get("rewrite_count", 0) + 1, }3.4 条件边:自我修正闭环的关键
条件边决定评分之后往哪走。有相关文档就生成,全部不相关且未超上限就改写,超限则走兜底:
# agents/edges.py from agents.state import AgentState def grade_edge(state: AgentState) -> str: if "relevant" in state["grade_results"]: return "generate" if state.get("rewrite_count", 0) < 3: return "rewrite" return "web_fallback"3.5 MCP 服务接入配置
MCP 的价值在于把数据库、通知、工单系统统一成协议化工具,新增数据源不用重写调用代码。下面是一个 MCP 网关的配置片段,用 stdio 方式启动本地 MCP Server:
{ "mcpServers": { "business-gateway": { "command": "python", "args": ["-m", "mcp_server.gateway"], "env": { "DB_DSN": "postgresql://agent:pass@db:5432/ops", "NOTIFY_ENDPOINT": "http://notify-svc:8080/send" } } } }在 LangGraph 里加载 MCP 工具并注册给 Agent:
# tools/mcp_loader.py from langchain_mcp_adapters.client import MultiServerMCPClient async def load_mcp_tools(): client = MultiServerMCPClient({ "business-gateway": { "command": "python", "args": ["-m", "mcp_server.gateway"], "transport": "stdio", } }) return await client.get_tools()3.6 图结构接线
把节点和边组装成图,注意 grader 之后走条件边:
# agents/graph.py from langgraph.graph import StateGraph, END from agents.state import AgentState from agents.nodes import router_node, grader_node, rewriter_node from agents.edges import grade_edge def build_graph(retriever_node, generator_node, fallback_node): g = StateGraph(AgentState) g.add_node("router", router_node) g.add_node("retriever", retriever_node) g.add_node("grader", grader_node) g.add_node("rewriter", rewriter_node) g.add_node("generate", generator_node) g.add_node("web_fallback", fallback_node) g.set_entry_point("router") g.add_edge("router", "retriever") g.add_edge("retriever", "grader") g.add_conditional_edges("grader", grade_edge, { "generate": "generate", "rewrite": "rewriter", "web_fallback": "web_fallback", }) g.add_edge("rewriter", "router") g.add_edge("generate", END) g.add_edge("web_fallback", END) return g.compile()4. 验证请求:从检索命中到业务动作触发
4.1 先验证模型通道
在跑完整图之前,先用一段最小请求确认 TaoToken 通道正常:
from config.llm import llm resp = llm.invoke("用一句话说明什么是 Agentic RAG") print(resp.content)如果返回正常文本,说明 Key 和 base_url 配置无误。若报 401,检查环境变量是否被 shell 覆盖;若报 404,检查 base_url 是否误加了路径后缀。
4.2 验证检索与评分闭环
构造一个知识库,故意让第一次检索命中不相关文档,观察改写是否触发:
import asyncio from agents.graph import build_graph from tools.mcp_loader import load_mcp_tools async def main(): tools = await load_mcp_tools() app = build_graph(retriever_node, generator_node, fallback_node) result = await app.ainvoke({ "question": "上周华东区退货率异常原因是什么", "rewrite_count": 0, "grade_results": [], }) print("路由:", result["route"]) print("改写次数:", result["rewrite_count"]) print("答案:", result["generation"]) asyncio.run(main())预期现象:第一次评分全部 irrelevant,rewrite_count 变为 1,问题被改写为更具体的检索式,第二轮命中相关文档后进入 generate。
4.3 验证业务动作触发
在生成节点之后接一个动作节点,通过 MCP 工具发通知。这里用结构化输出抽取动作参数,再由代码层执行,保证确定性:
from pydantic import BaseModel from config.llm import llm class NotifyAction(BaseModel): channel: str message: str def action_node(state: AgentState) -> AgentState: action = llm.with_structured_output(NotifyAction).invoke( f"根据以下结论生成一条通知:\n{state['generation']}" ) # 调用 MCP 工具,实际项目中替换为 await tool.ainvoke(...) result = f"已通过 {action.channel} 发送:{action.message[:30]}..." return {**state, "action_result": result}把 action_node 接到 generate 之后,再跑一次完整流程,你会看到输出里同时包含答案和动作执行结果。到这一步,从检索命中到业务动作触发的闭环就打通了。
5. 本篇常见错排查
报错一:openai.AuthenticationError: 401多数是环境变量没生效。在 Python 里打印os.environ.get("TAOTOKEN_API_KEY")确认,注意不要在代码里写死 Key 后又忘了导出。
报错二:NotFoundError: 404base_url 写成了https://taotoken.net/api/v1或带了查询串。统一用https://taotoken.net/api,SDK 会自己拼/chat/completions。
报错三:Agent 无限循环grade_edge 里忘了判断 rewrite_count 上限,或者 rewriter 节点没有累加计数。检查state.get("rewrite_count", 0) < 3这行是否生效。
报错四:MCP 工具加载为空stdio 方式启动时,command 和 args 必须能在当前工作目录下执行。先在终端手动跑一遍python -m mcp_server.gateway,确认没有 ImportError 再接入。
报错五:结构化输出解析失败模型返回了多余文本。确认使用的是支持 function calling 的模型,并在 with_structured_output 里显式传入 Pydantic 模型,不要用 dict。
报错六:检索一直不相关Embedding 模型和知识库切分粒度不匹配。chunk_size 建议 400 到 600,overlap 50 左右,太大导致语义稀释,太小导致上下文断裂。
6. 下一步:把闭环接到你的业务系统
到这里你已经有了一个可运行的骨架:LangGraph 负责状态流转和自我修正,MCP 负责工具标准化接入,TaoToken 负责模型通道。接下来要做的,是把 retriever_node 换成你真实的向量库,把 MCP 网关里的工具换成你的工单、审批、通知接口。
接入过程中如果遇到模型调用或工具注册的问题,优先查接入文档:
https://taotoken.net/doc?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content=doc需要新建或轮换密钥时走 API Keys 页面:
https://taotoken.net/api-keys?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content=api-keys如果你的 Agent 要长期跑编码类任务,比如自动修 bug、跑测试、提 PR,Coding Plan 比按次调用更合适:
https://taotoken.net/coding-plan?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content=coding-plan最后提醒一句:企业级 Agent 的难点从来不是模型多强,而是状态可观测、动作可回滚、失败可重试。把这三件事在 LangGraph 里显式建模,比换更大的模型有用得多。