很多大模型项目的问题,不是模型不会回答,而是系统无法解释、无法恢复、无法评估,也无法在真实业务中承担责任。
真正的智能体系统,核心并不是“让模型多调用几个工具”,而是让模型在明确的状态、流程、权限和反馈机制下工作。

一、LangChain 与 LangGraph 分别解决什么问题?

LangChain 更像是大模型应用的组件生态,主要负责:

  • 模型调用
  • Prompt 管理
  • 工具封装
  • RAG 检索
  • 输出解析
  • 多种数据源和模型集成

LangGraph 关注的是更复杂的运行时问题:

  • 多步骤工作流
  • 状态保存
  • 条件分支
  • 循环和重试
  • 人工审批
  • 中断与恢复
  • 多智能体协作

可以简单理解为:

LangChain:提供智能体所需要的零部件

LangGraph:决定这些零部件如何按照流程协作

传统 Chain 通常是线性的:

输入 -> Prompt -> 模型 -> 输出

而生产级 Agent 更接近:

输入
  -> 意图识别
  -> 路由
  -> 检索或工具调用
  -> 生成答案
  -> 风险检查
  -> 人工审批
  -> 输出

这也是 LangGraph 的价值所在。


二、一个生产级 Agent 应该具备什么能力?

一个可以上线的智能体,至少应该满足以下条件:

  1. 能够明确知道当前处于哪个步骤
  2. 能够保存和恢复执行状态
  3. 能够根据不同条件选择不同路径
  4. 能够在高风险操作前暂停
  5. 能够记录检索来源和执行轨迹
  6. 能够对回答质量进行评估
  7. 能够限制工具权限和执行范围

因此,Agent 的核心状态不应该只是聊天记录,而应该是一个结构化对象:

{
    "query": "用户问题",
    "route": "knowledge",
    "context": ["检索结果"],
    "answer": "模型草稿",
    "risk": "low",
    "approved": False,
    "citations": ["制度文档"]
}

聊天记录只是状态的一部分。


三、安装依赖

pip install -U langchain langgraph langchain-openai langchain-text-splitters

设置模型密钥:

# Windows PowerShell
$env:OPENAI_API_KEY="your-api-key"

# Linux 或 macOS
export OPENAI_API_KEY="your-api-key"

下面的代码使用 ChatOpenAI,如果使用其他模型,只需要替换模型初始化部分。


四、使用 LangGraph 构建一个生产级问答 Agent

这个示例包含以下节点:

知识库问题

普通问题

低风险

高风险

通过

拒绝

用户请求

问题标准化

意图路由

检索文档

直接回答

生成答案

风险检查

输出结果

人工审批

拒绝执行

1. 定义状态

from typing import Annotated, Literal
from typing_extensions import TypedDict

from langchain_core.messages import BaseMessage
from langchain_core.documents import Document
from langgraph.graph.message import add_messages


class AgentState(TypedDict, total=False):
    # 对话消息,add_messages 负责自动合并消息
    messages: Annotated[list[BaseMessage], add_messages]

    # 用户当前问题
    query: str

    # 路由结果
    route: Literal["knowledge", "general"]

    # 检索到的文档
    context: list[Document]

    # 生成的答案
    answer: str

    # 引用来源
    citations: list[str]

    # 风险等级
    risk: Literal["low", "high"]

    # 是否通过人工审批
    approved: bool

    # 风险原因
    review_reason: str

这里有一个非常重要的设计原则:

状态字段应该表达业务事实,而不是简单复制模型输出。

例如,approved 是一个业务决策状态,不能只依赖模型生成的自然语言。


2. 初始化模型和向量库

from langchain_openai import ChatOpenAI, OpenAIEmbeddings
from langchain_core.vectorstores import InMemoryVectorStore


llm = ChatOpenAI(
    model="gpt-4o-mini",
    temperature=0
)

embeddings = OpenAIEmbeddings(
    model="text-embedding-3-small"
)

documents = [
    Document(
        page_content=(
            "员工国内出差住宿标准:一线城市每晚不超过800元,"
            "其他城市每晚不超过500元。超出标准需要提交特殊审批。"
        ),
        metadata={"source": "company_travel_policy.md"}
    ),
    Document(
        page_content=(
            "差旅报销需要提供发票、出差申请单和行程证明。"
            "缺少任一材料时,财务部门可以退回报销申请。"
        ),
        metadata={"source": "company_reimbursement_policy.md"}
    ),
    Document(
        page_content=(
            "生产环境的数据库删除、权限修改和资金相关操作,"
            "必须经过人工审核后才能执行。"
        ),
        metadata={"source": "company_security_policy.md"}
    )
]

vector_store = InMemoryVectorStore(embeddings)
vector_store.add_documents(documents)

retriever = vector_store.as_retriever(
    search_kwargs={"k": 3}
)

真实项目中,文档通常需要先经过切分:

from langchain_text_splitters import RecursiveCharacterTextSplitter

splitter = RecursiveCharacterTextSplitter(
    chunk_size=500,
    chunk_overlap=80
)

chunks = splitter.split_documents(documents)
vector_store.add_documents(chunks)

文档切分不是越小越好。

切分过小会导致语义不完整,切分过大又会降低检索准确率。应根据文档类型调整:

  • API 文档:按照接口或章节切分
  • 产品手册:按照功能模块切分
  • 法律制度:按照条款切分
  • 源代码:按照类、函数和模块切分

3. 问题标准化节点

from langchain_core.messages import HumanMessage


def get_latest_question(messages: list[BaseMessage]) -> str:
    for message in reversed(messages):
        if isinstance(message, HumanMessage):
            return str(message.content)

    return ""


def normalize_node(state: AgentState) -> dict:
    query = get_latest_question(state.get("messages", []))

    return {
        "query": query.strip()
    }

问题标准化可以继续扩展为:

  • 去除无意义前缀
  • 补全上下文
  • 识别用户身份
  • 提取租户信息
  • 过滤敏感数据
  • 统一日期和金额格式

例如:

“那报销呢?”

单独看没有明确含义,但结合上一轮对话,可能指“住宿费用如何报销”。


4. 使用结构化输出进行路由

from pydantic import BaseModel, Field
from langchain_core.messages import SystemMessage


class RouteDecision(BaseModel):
    route: Literal["knowledge", "general"] = Field(
        description="knowledge 表示需要查询企业知识库,general 表示普通对话"
    )


router_model = llm.with_structured_output(RouteDecision)


def route_node(state: AgentState) -> dict:
    decision = router_model.invoke([
        SystemMessage(content=(
            "你是一个问题路由器。"
            "涉及公司制度、流程、报销、权限、安全规范的问题,"
            "必须选择 knowledge。普通闲聊和通用知识选择 general。"
        )),
        HumanMessage(content=state["query"])
    ])

    return {
        "route": decision.route
    }

为什么要使用结构化输出?

因为直接让模型返回:

这个问题应该查询知识库。

程序很难稳定解析。

而结构化输出可以将模型结果约束为:

{
  "route": "knowledge"
}

需要注意的是,路由模型的判断不应该直接决定权限。例如:

模型判断:用户可以删除生产数据库

这不能作为真正的授权依据。

路由可以交给模型,权限必须交给确定性的业务规则。


5. 检索节点

def retrieve_node(state: AgentState) -> dict:
    docs = retriever.invoke(state["query"])

    citations = []
    for doc in docs:
        source = str(doc.metadata.get("source", "unknown"))

        if source not in citations:
            citations.append(source)

    return {
        "context": docs,
        "citations": citations
    }

一个常见错误是只把检索结果拼到 Prompt 中,却不记录来源。

这样会导致:

  • 用户无法验证答案
  • 开发者无法追踪错误
  • 评估系统无法判断引用是否正确
  • 出现幻觉时难以定位原因

生产系统应该把以下信息保留下来:

{
    "source": "company_travel_policy.md",
    "chunk_id": "travel-001",
    "score": 0.89,
    "retrieved_at": "2026-08-08T10:00:00"
}

6. 答案生成节点

from langchain_core.messages import AIMessage


def draft_node(state: AgentState) -> dict:
    docs = state.get("context", [])

    if docs:
        context = "\n\n".join(
            f"[文档{i + 1}]\n{doc.page_content}"
            for i, doc in enumerate(docs)
        )
    else:
        context = "当前没有可用的企业内部文档。"

    prompt = f"""
你是一名严谨的企业知识库助手。

请根据以下资料回答问题:

{context}

用户问题:
{state["query"]}

回答要求:
1. 只能使用资料中明确出现的信息。
2. 如果资料不足,明确说明无法确认。
3. 不要编造制度、金额、时间和权限。
4. 涉及操作执行时,只能给出说明,不能假装已经执行。
5. 回答末尾列出使用的文档来源。
"""

    response = llm.invoke([
        SystemMessage(content="你必须忠实依据资料回答问题。"),
        HumanMessage(content=prompt)
    ])

    return {
        "answer": str(response.content)
    }

这里最重要的一句话是:

如果资料不足,明确说明无法确认。

RAG 系统真正的质量,不是“每个问题都回答”,而是:

在知道答案时准确回答,在不知道答案时诚实拒答。


7. 风险检查节点

不要把所有风险判断都交给大模型。

对于删除、转账、改权限、修改生产数据等操作,应当先通过确定性规则拦截:

HIGH_RISK_TERMS = (
    "删除",
    "转账",
    "退款",
    "修改权限",
    "生产数据库",
    "密码",
    "密钥",
    "delete",
    "transfer",
    "production database"
)


def risk_gate_node(state: AgentState) -> dict:
    text = (
        state.get("query", "") + "\n" +
        state.get("answer", "")
    ).lower()

    matched_terms = [
        term for term in HIGH_RISK_TERMS
        if term.lower() in text
    ]

    if matched_terms:
        return {
            "risk": "high",
            "review_reason": (
                "检测到高风险关键词:"
                + ", ".join(matched_terms)
            )
        }

    return {
        "risk": "low",
        "review_reason": ""
    }

这段代码并不能替代完整的安全系统,但它体现了一个基本原则:

模型负责理解,规则负责兜底,权限系统负责最终决定。

五、人工介入与可恢复执行

LangGraph 支持在流程中暂停,等待人工处理后继续执行。

from langgraph.types import interrupt


def human_review_node(state: AgentState) -> dict:
    decision = interrupt({
        "type": "approval_required",
        "reason": state.get("review_reason", ""),
        "query": state["query"],
        "answer": state.get("answer", "")
    })

    approved = False

    if isinstance(decision, dict):
        approved = bool(decision.get("approved", False))

    return {
        "approved": approved
    }

当风险较高时,将流程转入人工审批:

def risk_router(state: AgentState) -> str:
    if state.get("risk") == "high":
        return "human_review"

    return "finalize"


def review_router(state: AgentState) -> str:
    if state.get("approved"):
        return "finalize"

    return "blocked"

人工审批不是简单地弹出一个确认框,而是要保留完整上下文:

{
  "type": "approval_required",
  "reason": "检测到修改生产数据库操作",
  "query": "删除生产环境中的订单数据",
  "answer": "该操作需要管理员审核"
}

审批人应该能够看到:

  • 用户是谁
  • 请求来源是什么
  • 模型准备执行什么
  • 使用了哪些工具
  • 影响范围多大
  • 是否存在敏感数据
  • 是否可以回滚

六、组装完整工作流

from langgraph.graph import StateGraph, START, END


def finalize_node(state: AgentState) -> dict:
    return {
        "messages": [
            AIMessage(content=state.get("answer", "没有生成有效答案。"))
        ]
    }


def blocked_node(state: AgentState) -> dict:
    return {
        "answer": (
            "该请求涉及高风险操作,尚未通过人工审核。"
            "系统未执行任何外部操作。"
        )
    }


workflow = StateGraph(AgentState)

workflow.add_node("normalize", normalize_node)
workflow.add_node("route", route_node)
workflow.add_node("retrieve", retrieve_node)
workflow.add_node("draft", draft_node)
workflow.add_node("risk_gate", risk_gate_node)
workflow.add_node("human_review", human_review_node)
workflow.add_node("finalize", finalize_node)
workflow.add_node("blocked", blocked_node)

workflow.add_edge(START, "normalize")
workflow.add_edge("normalize", "route")

workflow.add_conditional_edges(
    "route",
    lambda state: state["route"],
    {
        "knowledge": "retrieve",
        "general": "draft"
    }
)

workflow.add_edge("retrieve", "draft")
workflow.add_edge("draft", "risk_gate")

workflow.add_conditional_edges(
    "risk_gate",
    risk_router,
    {
        "human_review": "human_review",
        "finalize": "finalize"
    }
)

workflow.add_conditional_edges(
    "human_review",
    review_router,
    {
        "finalize": "finalize",
        "blocked": "blocked"
    }
)

workflow.add_edge("blocked", END)
workflow.add_edge("finalize", END)

到这里,我们已经把一个线性的问答流程变成了一个带有:

  • 路由
  • 检索
  • 状态
  • 风险检查
  • 人工审批
  • 条件分支
  • 最终输出

的完整图结构。


七、状态持久化与多轮对话

开发阶段可以使用内存检查点:

from langgraph.checkpoint.memory import MemorySaver


memory = MemorySaver()

app = workflow.compile(
    checkpointer=memory
)

调用时必须传入 thread_id

from langchain_core.messages import HumanMessage


config = {
    "configurable": {
        "thread_id": "user-1001-session-001"
    }
}

result = app.invoke(
    {
        "messages": [
            HumanMessage(
                content="国内出差住宿费用的标准是什么?"
            )
        ]
    },
    config=config
)

print(result["answer"])
print(result.get("citations", []))

thread_id 的含义不是用户 ID,而是一次可恢复的会话执行标识。

同一个 thread_id 可以让系统恢复之前的状态:

result = app.invoke(
    {
        "messages": [
            HumanMessage(content="那报销时还需要什么材料?")
        ]
    },
    config=config
)

生产环境不应使用 MemorySaver 作为唯一存储,因为进程重启后状态会丢失。

生产部署需要使用持久化 Checkpointer,例如数据库或其他可靠存储,并重点考虑:

  • 状态版本
  • 并发更新
  • 租户隔离
  • 数据过期
  • 敏感字段加密
  • 审批记录保留
  • 失败任务重试

八、人工审批后的恢复执行

高风险请求触发中断后,前端可以展示审批信息:

config = {
    "configurable": {
        "thread_id": "risk-session-001"
    }
}

first_result = app.invoke(
    {
        "messages": [
            HumanMessage(
                content="请删除生产数据库中三个月前的订单"
            )
        ]
    },
    config=config
)

print(first_result)

审批通过后,恢复执行:

from langgraph.types import Command


final_result = app.invoke(
    Command(
        resume={
            "approved": True
        }
    ),
    config=config
)

print(final_result["answer"])

审批拒绝:

final_result = app.invoke(
    Command(
        resume={
            "approved": False
        }
    ),
    config=config
)

需要特别强调:

人工审批节点之后,不应该直接执行任意模型生成的代码。

真正执行工具时,仍然需要进行:

  • 用户授权检查
  • 参数校验
  • 资源范围校验
  • 幂等性检查
  • 审计日志记录
  • 超时控制
  • 回滚或补偿

九、工具调用的安全边界

工具不应该无限开放给模型。

错误做法:

tools = [
    database_tool,
    shell_tool,
    file_system_tool,
    payment_tool
]

这种设计相当于让模型拥有一个不受限制的操作系统入口。

更合理的方式是定义白名单:

ALLOWED_TOOLS = {
    "query_order",
    "search_policy",
    "get_user_profile"
}


def authorize_tool(tool_name: str, user_roles: set[str]) -> bool:
    if tool_name not in ALLOWED_TOOLS:
        return False

    if tool_name == "get_user_profile":
        return "hr" in user_roles or "admin" in user_roles

    return True

对于工具参数也要进行校验:

def validate_order_id(order_id: str) -> str:
    if not order_id.isalnum():
        raise ValueError("订单号格式不合法")

    if len(order_id) > 32:
        raise ValueError("订单号长度非法")

    return order_id

模型输出的参数永远是不可信输入。


十、RAG 系统最容易出现的三个问题

1. 检索到了错误文档

原因可能包括:

  • 文档切分不合理
  • 向量模型不适合当前语言
  • 查询问题过短
  • 没有使用元数据过滤
  • 相似度阈值设置不合理

解决思路:

retriever = vector_store.as_retriever(
    search_type="similarity",
    search_kwargs={
        "k": 4
    }
)

企业知识库还应该增加租户和权限过滤:

metadata = {
    "tenant_id": "tenant-a",
    "department": "finance",
    "visibility": "internal"
}

用户没有权限看到的文档,不能因为“相似度高”就返回给模型。

2. 检索到了正确文档,但答案仍然错误

可能是模型:

  • 混合了多个文档的结论
  • 忽略了时间范围
  • 误解了否定条件
  • 把建议当成制度
  • 把历史版本当成当前版本

因此 Prompt 中应该明确:

当多个文档存在冲突时,优先使用发布日期最新且状态为有效的文档。
如果无法判断哪个版本有效,请明确说明存在冲突。

3. 答案看起来正确,但没有依据

解决方式是强制引用:

请在答案结尾输出:

参考来源:
- 文档名称
- 条款或章节

如果系统无法提供来源,就应该降低答案可信等级。


十一、如何评估 Agent,而不是凭感觉测试?

很多项目上线前只问几个问题:

“你好”
“公司报销标准是什么?”
“帮我总结这篇文档”

这不能证明系统可靠。

至少应该建立测试集:

test_cases = [
    {
        "id": "travel-001",
        "question": "一线城市出差住宿标准是多少?",
        "expected_sources": ["company_travel_policy.md"],
        "expected_keywords": ["800元"]
    },
    {
        "id": "refund-001",
        "question": "报销需要哪些材料?",
        "expected_sources": ["company_reimbursement_policy.md"],
        "expected_keywords": ["发票"]
    }
]

编写基础评估函数:

def evaluate_case(case: dict) -> dict:
    config = {
        "configurable": {
            "thread_id": f"eval-{case['id']}"
        }
    }

    result = app.invoke(
        {
            "messages": [
                HumanMessage(content=case["question"])
            ]
        },
        config=config
    )

    answer = result.get("answer", "")
    citations = set(result.get("citations", []))

    source_hit = bool(
        citations.intersection(case["expected_sources"])
    )

    keyword_hit = all(
        keyword in answer
        for keyword in case["expected_keywords"]
    )

    return {
        "id": case["id"],
        "source_hit": source_hit,
        "keyword_hit": keyword_hit,
        "answer": answer
    }


results = [
    evaluate_case(case)
    for case in test_cases
]

for item in results:
    print(item)

基础评估指标可以包括:

路由准确率
检索命中率
引用准确率
答案相关性
答案完整性
拒答准确率
工具调用成功率
人工审批触发准确率
平均响应时间
平均 Token 消耗

不要只使用“模型评审模型”。

LLM-as-a-Judge 可以辅助评估,但不应该成为唯一标准。业务规则、关键词、来源匹配和人工抽检仍然不可替代。


十二、可观测性比 Prompt 更重要

生产系统必须知道一次请求经历了什么:

用户问题
  -> 使用了哪个模型
  -> 选择了哪条路由
  -> 检索了哪些文档
  -> 调用了哪些工具
  -> 花费了多少 Token
  -> 哪一步失败
  -> 是否触发人工审核
  -> 最终结果是什么

可以为每次调用增加元数据:

config = {
    "configurable": {
        "thread_id": "user-1001-session-001"
    },
    "tags": [
        "production",
        "knowledge-agent"
    ],
    "metadata": {
        "tenant_id": "tenant-a",
        "user_id": "user-1001",
        "request_id": "req-20260808-001"
    }
}

日志中不应该直接记录:

  • 用户密码
  • API Key
  • 身份证号
  • 银行卡号
  • 完整客户隐私信息

应在日志层进行脱敏:

import re


def mask_phone(text: str) -> str:
    return re.sub(
        r"(1\d{2})\d{4}(\d{4})",
        r"\1****\2",
        text
    )

日志的目标不是“记录越多越好”,而是:

在不泄露隐私的前提下,让问题可以被复现和定位。


十三、LangChain 与 LangGraph 的工程分层

一个更清晰的项目结构可以是:

agent_project/
├── app.py
├── graph/
│   ├── state.py
│   ├── nodes.py
│   ├── routes.py
│   └── workflow.py
├── retrieval/
│   ├── ingest.py
│   ├── retriever.py
│   └── filters.py
├── tools/
│   ├── order.py
│   ├── user.py
│   └── permissions.py
├── evaluation/
│   ├── dataset.json
│   └── evaluate.py
└── security/
    ├── policy.py
    └── masking.py

推荐的职责划分:

LangChain:
模型、Prompt、Retriever、Tool、Parser

LangGraph:
State、Node、Edge、Checkpoint、Interrupt

业务代码:
权限、审计、数据过滤、异常处理、指标评估

不要把所有逻辑都塞进一个 Agent 节点中。

一个几百行的“超级 Agent”往往比多个职责清晰的小节点更难维护。


十四、从 Demo 到生产需要跨越的几个阶段

阶段一:验证模型能力

目标是确认:

  • 模型是否理解业务问题
  • RAG 是否能够召回相关内容
  • 工具调用格式是否正确

这个阶段可以使用简单 Chain。

阶段二:固定工作流

当流程开始出现分支、重试和状态时,引入 LangGraph:

路由 -> 检索 -> 生成 -> 校验 -> 输出

阶段三:增加可恢复能力

加入:

  • Checkpointer
  • thread_id
  • 失败重试
  • 人工中断
  • 任务恢复

阶段四:增加治理能力

加入:

  • 权限控制
  • 审计日志
  • 数据脱敏
  • 工具白名单
  • 评估数据集
  • 成本和延迟监控

阶段五:持续迭代

每一次线上错误,都应该沉淀为新的测试样例:

线上问题 -> 归因 -> 新增测试用例 -> 修复 -> 回归评估

这才是 Agent 系统不断变可靠的过程。


十五、几个关键结论

LangChain 解决的是“大模型应用如何组装”的问题。

LangGraph 解决的是“复杂智能体如何可靠运行”的问题。

但它们都不能自动解决:

  • 业务权限
  • 数据安全
  • 结果正确性
  • 生产运维
  • 责任边界

真正成熟的智能体系统,不是让模型拥有更多自由,而是让模型在清晰的边界内完成更多工作。

可以用一句话概括:

LangChain 负责能力,LangGraph 负责流程,业务系统负责约束。

如果一个 Agent 只能在理想输入下回答问题,它只是 Demo。

如果它能够在信息不足时拒答、在流程异常时恢复、在高风险操作前暂停、在出现错误后被评估和追踪,它才真正接近生产系统。


写在最后

大模型应用的竞争,正在从“谁能调用模型”转向“谁能把模型变成可靠的软件系统”。

LangChain 让开发者可以快速构建模型、工具和知识库能力;LangGraph 则进一步把这些能力组织成具有状态、分支、记忆和人工控制的执行图。

但真正决定系统能否上线的,不是模型回答得多么精彩,而是:

  • 是否能解释答案从哪里来
  • 是否能知道什么时候不该回答
  • 是否能在失败后继续运行
  • 是否能限制模型的权限
  • 是否能用数据证明系统正在变好

这才是从 Demo 走向生产的真正分水岭。

Logo

欢迎加入 MCP 技术社区!与志同道合者携手前行,一同解锁 MCP 技术的无限可能!

更多推荐