超越玩具 Agent:基于 LangGraph 构建具备 RAG 兜底与 Human-in-the-loop 的可控业务状态机

超越玩具 Agent:基于 LangGraph 构建具备 RAG 兜底与 Human-in-the-loop 的可控业务状态机

在实验室或本地写 Demo 时,基于 LCEL(LangChain Expression Language)搭建一条 Prompt | LLM | Tool 或者简单的 VectorStore | Retriever | StuffDocumentsChain 只需要十几行代码。这类线性管道在面对确定性输入时表现尚可,但在真实生产环境中,单纯依赖大模型的自治能力几乎必然会导致系统失控。

真实企业级交付场景面临几个难以回避的工程断层:

  1. 检索失效时的不可控幻觉:向量数据库召回的分数极低,模型在缺乏有效上下文时开始胡编乱造,甚至给出错误的业务政策解答。
  2. 高危操作的越权风险:当用户输入带有破坏性或不可逆的操作意图(如"注销当前账户"、"清空所有关联数据")时,无审查机制的 Tool Calling 会直接执行底层接口。
  3. 长流程业务与人机协同(HITL)的割裂:真实的业务往往包含审批流------用户今天提交退款申请,客服主管可能明天下午才在后台完成审核。传统的同步阻塞式调用根本无法支撑跨越数小时甚至数天的状态挂起与断点恢复。

要解决这些问题,必须将 Agent 从"单向线性管道"重构为"具备状态持久化、动态可信度评估与人机协同机制的有向状态图"。下面我将拆解如何利用 LangGraph 落地一套包含 RAG 兜底、人工审批与审计留痕的可控业务系统。


系统状态建模与双环拓扑

构建复杂 Agent 系统的第一步不是写 Prompt,而是抽象全局状态(State)。在多步骤与高危审批流程中,仅靠简单的 messages 列表无法承载复杂的业务元数据。

我将整个系统的运行状态定义为一个结构化模型。除继承基础会话消息外,还需要引入路由意图、检索可信度指标、工单元数据,以及追加式写入的审计日志

python 复制代码
import operator
from typing import Annotated, Optional
from langgraph.graph import MessagesState


class ExtendedState(MessagesState):
    # 意图识别与风险感知
    question: str
    intent: str
    need_retrieval: bool
    risk_level: str  # "normal" | "sensitive" | "dangerous"

    # RAG 检索上下文与评估指标
    documents: list[dict]
    sources: list[dict]
    retrieval_reliable: bool

    # 兜底工单字段
    ticket_title: str
    ticket_description: str
    ticket_id: Optional[str]
    approval_status: Optional[str]  # "pending" | "approved" | "rejected"

    # 关键:审计日志使用追加式 Reducer,保障操作轨迹的单调不可逆性
    audit_log: Annotated[list[dict], operator.add]

整个状态机的流转拓扑包含"知识验证闭环"与"人机协同闭环"两个核心子系统:
#mermaid-svg-bNkEyImUZ5YwoBPg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-bNkEyImUZ5YwoBPg .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-bNkEyImUZ5YwoBPg .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-bNkEyImUZ5YwoBPg .error-icon{fill:#552222;}#mermaid-svg-bNkEyImUZ5YwoBPg .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-bNkEyImUZ5YwoBPg .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-bNkEyImUZ5YwoBPg .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-bNkEyImUZ5YwoBPg .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-bNkEyImUZ5YwoBPg .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-bNkEyImUZ5YwoBPg .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-bNkEyImUZ5YwoBPg .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-bNkEyImUZ5YwoBPg .marker{fill:#333333;stroke:#333333;}#mermaid-svg-bNkEyImUZ5YwoBPg .marker.cross{stroke:#333333;}#mermaid-svg-bNkEyImUZ5YwoBPg svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-bNkEyImUZ5YwoBPg p{margin:0;}#mermaid-svg-bNkEyImUZ5YwoBPg .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-bNkEyImUZ5YwoBPg .cluster-label text{fill:#333;}#mermaid-svg-bNkEyImUZ5YwoBPg .cluster-label span{color:#333;}#mermaid-svg-bNkEyImUZ5YwoBPg .cluster-label span p{background-color:transparent;}#mermaid-svg-bNkEyImUZ5YwoBPg .label text,#mermaid-svg-bNkEyImUZ5YwoBPg span{fill:#333;color:#333;}#mermaid-svg-bNkEyImUZ5YwoBPg .node rect,#mermaid-svg-bNkEyImUZ5YwoBPg .node circle,#mermaid-svg-bNkEyImUZ5YwoBPg .node ellipse,#mermaid-svg-bNkEyImUZ5YwoBPg .node polygon,#mermaid-svg-bNkEyImUZ5YwoBPg .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-bNkEyImUZ5YwoBPg .rough-node .label text,#mermaid-svg-bNkEyImUZ5YwoBPg .node .label text,#mermaid-svg-bNkEyImUZ5YwoBPg .image-shape .label,#mermaid-svg-bNkEyImUZ5YwoBPg .icon-shape .label{text-anchor:middle;}#mermaid-svg-bNkEyImUZ5YwoBPg .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-bNkEyImUZ5YwoBPg .rough-node .label,#mermaid-svg-bNkEyImUZ5YwoBPg .node .label,#mermaid-svg-bNkEyImUZ5YwoBPg .image-shape .label,#mermaid-svg-bNkEyImUZ5YwoBPg .icon-shape .label{text-align:center;}#mermaid-svg-bNkEyImUZ5YwoBPg .node.clickable{cursor:pointer;}#mermaid-svg-bNkEyImUZ5YwoBPg .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-bNkEyImUZ5YwoBPg .arrowheadPath{fill:#333333;}#mermaid-svg-bNkEyImUZ5YwoBPg .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-bNkEyImUZ5YwoBPg .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-bNkEyImUZ5YwoBPg .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-bNkEyImUZ5YwoBPg .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-bNkEyImUZ5YwoBPg .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-bNkEyImUZ5YwoBPg .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-bNkEyImUZ5YwoBPg .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-bNkEyImUZ5YwoBPg .cluster text{fill:#333;}#mermaid-svg-bNkEyImUZ5YwoBPg .cluster span{color:#333;}#mermaid-svg-bNkEyImUZ5YwoBPg div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-bNkEyImUZ5YwoBPg .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-bNkEyImUZ5YwoBPg rect.text{fill:none;stroke-width:0;}#mermaid-svg-bNkEyImUZ5YwoBPg .icon-shape,#mermaid-svg-bNkEyImUZ5YwoBPg .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-bNkEyImUZ5YwoBPg .icon-shape p,#mermaid-svg-bNkEyImUZ5YwoBPg .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-bNkEyImUZ5YwoBPg .icon-shape .label rect,#mermaid-svg-bNkEyImUZ5YwoBPg .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-bNkEyImUZ5YwoBPg .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-bNkEyImUZ5YwoBPg .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-bNkEyImUZ5YwoBPg :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 危险操作
需要检索
常规对话
可靠
不可靠
审批通过
审批驳回
审批通过/驳回
START
意图与风险识别
评估风险与意图
高危拦截审批
检索知识库
生成直接回复
评估检索可信度
是否可靠?
严格引用生成
生成工单草稿
挂起并等待人工审批
系统建单
拒绝建单并回复
记录高危审计
END


带有可信度门禁的 RAG 编排

常规 RAG 最致命的问题是"盲目信任检索器"。只要检索器吐出 Top-K,无论相似度多么微弱,数据都会被直接塞入 Prompt。

在 LangGraph 中,我把检索和生成拆解为独立节点,并在二者之间强行插入一个可信度门禁(Reliability Gate)

1. 结构化检索与来源抽离

检索节点不仅要把文档内容带入状态,还要将元数据(Document ID、Title、Score)分离出来,为下游的生成约束和前端卡片渲染做准备:

python 复制代码
def retrieve_node(state: ExtendedState) -> dict:
    question = state["question"]
    # 模拟向量检索或混合检索流程
    raw_docs = search_vector_store(question, top_k=3)

    sources = [
        {
            "id": doc["id"],
            "title": doc["title"],
            "content": doc["content"],
            "score": doc["score"],
        }
        for doc in raw_docs
    ]

    return {
        "documents": raw_docs,
        "sources": sources,
        "audit_log": [{"action": "retrieve", "query": question, "matched_count": len(raw_docs)}]
    }

2. 基于确定性规则的可信度判定

评估检索结果是否可靠,不应该完全依赖高延迟的大模型二次评判(LLM-as-a-Judge),使用确定性的阈值门禁往往更高效且行为可预期:

python 复制代码
def assess_retrieval_node(state: ExtendedState) -> dict:
    documents = state.get("documents", [])
    
    # 门禁规则:非空校验与最高相关度阈值过滤
    if not documents:
        reliable = False
    else:
        max_score = max(doc.get("score", 0.0) for doc in documents)
        # 假设标准化后的相似度得分阈值为 0.75
        reliable = max_score >= 0.75

    return {
        "retrieval_reliable": reliable,
        "audit_log": [{"action": "assess_retrieval", "is_reliable": reliable}]
    }

def route_after_retrieval(state: ExtendedState) -> str:
    if state["retrieval_reliable"]:
        return "generate_grounded"
    return "prepare_ticket"

3. 基于来源引用的受控生成

当判定检索可信时,提示词必须施加严格的形式约束,要求模型仅根据提供的上下文生成回答,并强制标注引用来源:

python 复制代码
from langchain_core.messages import SystemMessage, HumanMessage

def generate_grounded_node(state: ExtendedState, llm) -> dict:
    context_str = "\n\n".join(
        [f"[{doc['id']}] {doc['title']}:\n{doc['content']}" for doc in state["sources"]]
    )
    
    system_prompt = (
        "你是一个严谨的业务支持助手。你必须严格基于给定的参考资料回答问题。\n"
        "规则:\n"
        "1. 如果资料中没有明确提及答案,必须直接回答'根据已知知识库无法确定',严禁发散编造。\n"
        "2. 在阐述每一个事实依据时,必须在句尾标注对应的文档编号,如 [KB001]。"
    )
    
    user_prompt = f"参考资料:\n{context_str}\n\n用户问题:{state['question']}"
    
    response = llm.invoke([
        SystemMessage(content=system_prompt),
        HumanMessage(content=user_prompt)
    ])
    
    return {
        "messages": [response],
        "audit_log": [{"action": "generate_answer", "status": "success"}]
    }

确定性兜底:检索失败降级为工单

当可信度评估未通过时,系统绝对不能"硬着头皮回答"。此时正确的做法是退出生成流程,进入受控的降级逻辑------生成工单草稿。

很多团队在这里容易犯一个错误:让 LLM 去生成工单标题和描述。这引入了额外的随机性。对于涉及跨团队协作或工单系统的核心链路,工单核心字段应当由确定性业务规则拼接生成:

python 复制代码
def prepare_ticket_node(state: ExtendedState) -> dict:
    question = state["question"]
    
    # 严格使用规则兜底构建工单元数据
    title = f"[知识库无答案] 用户咨询:{question[:25]}"
    description = (
        f"工单产生原因:知识库检索置信度不足\n"
        f"用户原始提问:{question}\n"
        f"系统处置:自动触发转人工流转,等待运营或客服审批确认。"
    )

    return {
        "ticket_title": title,
        "ticket_description": description,
        "approval_status": "pending",
        "audit_log": [{"action": "prepare_ticket", "title": title}]
    }

基于 interrupt 与 Command 的人机协同(HITL)

如何优雅地处理人工审批?在传统后端架构中,通常需要手写一整套状态机服务,把上下文序列化存入 MySQL,待审批 API 被触发后再重新拼装状态。

LangGraph 1.0+ 提供了原生的断点机制:通过 interrupt() 函数实现状态图的挂起,通过 Command(resume=...) 实现状态唤醒与动态路由。
检查点存储 (Checkpointer) 状态图运行时 检查点存储 (Checkpointer) 状态图运行时 #mermaid-svg-2jxE8D6K7M73DKBH{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-2jxE8D6K7M73DKBH .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-2jxE8D6K7M73DKBH .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-2jxE8D6K7M73DKBH .error-icon{fill:#552222;}#mermaid-svg-2jxE8D6K7M73DKBH .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-2jxE8D6K7M73DKBH .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-2jxE8D6K7M73DKBH .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-2jxE8D6K7M73DKBH .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-2jxE8D6K7M73DKBH .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-2jxE8D6K7M73DKBH .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-2jxE8D6K7M73DKBH .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-2jxE8D6K7M73DKBH .marker{fill:#333333;stroke:#333333;}#mermaid-svg-2jxE8D6K7M73DKBH .marker.cross{stroke:#333333;}#mermaid-svg-2jxE8D6K7M73DKBH svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-2jxE8D6K7M73DKBH p{margin:0;}#mermaid-svg-2jxE8D6K7M73DKBH .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-2jxE8D6K7M73DKBH text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-2jxE8D6K7M73DKBH .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-2jxE8D6K7M73DKBH .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-2jxE8D6K7M73DKBH .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-2jxE8D6K7M73DKBH .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-2jxE8D6K7M73DKBH #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-2jxE8D6K7M73DKBH .sequenceNumber{fill:white;}#mermaid-svg-2jxE8D6K7M73DKBH #sequencenumber{fill:#333;}#mermaid-svg-2jxE8D6K7M73DKBH #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-2jxE8D6K7M73DKBH .messageText{fill:#333;stroke:none;}#mermaid-svg-2jxE8D6K7M73DKBH .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-2jxE8D6K7M73DKBH .labelText,#mermaid-svg-2jxE8D6K7M73DKBH .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-2jxE8D6K7M73DKBH .loopText,#mermaid-svg-2jxE8D6K7M73DKBH .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-2jxE8D6K7M73DKBH .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-2jxE8D6K7M73DKBH .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-2jxE8D6K7M73DKBH .noteText,#mermaid-svg-2jxE8D6K7M73DKBH .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-2jxE8D6K7M73DKBH .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-2jxE8D6K7M73DKBH .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-2jxE8D6K7M73DKBH .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-2jxE8D6K7M73DKBH .actorPopupMenu{position:absolute;}#mermaid-svg-2jxE8D6K7M73DKBH .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-2jxE8D6K7M73DKBH .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-2jxE8D6K7M73DKBH .actor-man circle,#mermaid-svg-2jxE8D6K7M73DKBH line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-2jxE8D6K7M73DKBH :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 进程退出 / 释放计算资源 经过数小时后,管理员在管理台看到待办 用户 业务管理员 (后台) 触发检索失败流程1执行 prepare_ticket2ticket_approval_node 触发 interrupt()3将当前 State 快照落盘持久化 (thread_id=T101)4POST /resume {thread_id: T101, resume: "approved"}5读取 thread_id=T101 的 State 快照6恢复运行时状态7interrupt() 接收外部值并继续执行8Command 路由至 create_ticket9返回工单创建结果10 用户 业务管理员 (后台)

1. 审批节点的挂起设计

在审批节点内部,直接调用 interrupt()。一旦执行到此处,LangGraph 会自动把当前的状态上下文序列化存入 Checkpointer,然后直接终止图执行并返回:

python 复制代码
from langgraph.types import interrupt, Command

def ticket_approval_node(state: ExtendedState):
    # 触发挂起,将上下文暴露给外部调用方
    decision = interrupt({
        "type": "ticket_approval_request",
        "ticket_title": state["ticket_title"],
        "ticket_description": state["ticket_description"],
        "prompt": "系统检索置信度不足,是否同意创建运维工单?"
    })

    # 唤醒后,根据管理员传入的 decision 决定流转路径
    if decision == "approved":
        return Command(
            goto="create_ticket",
            update={
                "approval_status": "approved",
                "audit_log": [{"action": "approval", "result": "approved"}]
            }
        )
    else:
        return Command(
            goto="reject_ticket",
            update={
                "approval_status": "rejected",
                "audit_log": [{"action": "approval", "result": "rejected"}]
            }
        )

2. 状态恢复与外部驱动

在服务层,调用方通过 thread_id 维持长会话生命周期。

python 复制代码
config = {"configurable": {"thread_id": "session_user_9527"}}

# 1. 用户发起提问,图执行至 interrupt 处自动挂起
app.invoke({"question": "公司对于远程办公的网络专线报销标准是多少?"}, config=config)

# 2. 此时检查状态机,会发现处于中断状态
current_state = app.get_state(config)
print("下一次待执行目标:", current_state.next)  # 输出包含 ticket_approval
print("中断挂起时的载荷:", current_state.tasks[0].interrupts)

# 3. 管理员在后台审核通过后,触发恢复调用
app.invoke(Command(resume="approved"), config=config)

高危操作的安全防御与不可篡改审计

对于涉及数据库变更、权限配置变更等高危输入,业务防御必须前置。当意图分类器识别出 risk_level == "dangerous" 时,控制流将被强行引流至高危拦截审批节点。

更为关键的是审计追溯。很多系统的审计日志只是零散打印在控制台,而我们将其建模为 State 中具备单调递增特性的切片:

python 复制代码
class ExtendedState(MessagesState):
    ...
    # 使用 operator.add 声明该字段只能追加,不可覆盖
    audit_log: Annotated[list[dict], operator.add]

无论下游节点发生异常还是被正常拒绝,先前产生的审计元数据都已经被 Checkpointer 固化在存储介质中:

python 复制代码
def log_audit_node(state: ExtendedState) -> dict:
    # 模拟对接公司内部统一审计网关或 Kafka
    for log_item in state["audit_log"]:
        write_to_compliance_system(log_item)
    
    return {
        "messages": [HumanMessage(content="您的请求包含敏感操作,已记录审计并拦截。")]
    }

即使用户的意图在后续被人为篡改,通过比对整个图的历史版本快照(app.get_state_history(config)),也能精准还原出每一次决策迁移的时间戳、节点输入与输出。


生产落地中的几点工程取舍

在将上述模式推向实际业务的过程中,有几个细节往往是书本或官方示例不会明确说明的:

  1. Checkpointer 的选型分水岭
  • MemorySaver 仅能用于跑单测。服务只要做一次常规发版或 Pod 重启,所有等待审批的中断状态就会彻底丢失。
  • 单机部署或边缘节点可使用 SqliteSaver
  • 生产环境必须上 PostgresSaver。在分布式环境下,多个 Pod 同时处理外部 Resume 请求时,底层数据库必须提供行级锁或基于版本号的乐观并发控制(OCC),防止同一笔审批被并发消费两次。
  1. 状态快照膨胀治理
    引入长程审批后,每次更新状态都会产生一条快照记录。如果 documents 中包含大量的大文本切片,数据库的体积会迅速膨胀。可以在构建 ExtendedState 时,将原始文档(documents)与元数据来源(sources)解耦,在完成检索评估后,主动清空大文本字段,仅保留轻量的 ID 和摘要。
  2. 规则与模型的职责边界
    越是靠近底层业务操作(鉴权、审批流、工单提交),越要坚决使用确定性的代码规则(Python if/else 与结构化解析);越是靠近非结构化语义理解(意图粗筛、语料泛化生成),才越应该交给大模型。

从简单的 LCEL 链式调用走向基于 LangGraph 的有向状态机,本质上是软件工程面对不确定性系统时的一种必然演进。当大模型输出的随机性被严密的门禁规则、确定性的状态迁移以及可追溯的 Checkpointer 牢牢约束在可控轨道内时,Agent 才真正具备了走出实验室、承载核心业务逻辑的可能。

相关推荐
炎黄盈动_20032 小时前
汽车零部件企业AI智能费控方案:私有化部署、数据安全与业财一体化实践
人工智能·低代码·ai
阆遤2 小时前
llama.cpp 0.4.0-dev 本地编译指南
ai·编译·llama.cpp
我是苏苏3 小时前
Agent 开发实战 01:C# 使用 Semantic Kernel 构建 AIAgent
ai
znnnk3 小时前
【AI应用】从 Prompt 到 Skill:AI 到底“会什么”?
ai·prompt·ai编程·ai应用·skill
ruleslol3 小时前
Function Calling 讲解
java·ai
RobinDevNotes3 小时前
TensorRT 与 ONNX Runtime 推理全解析
搜索引擎·ai·大模型·推理引擎
LuTshoes3 小时前
AI Agent 相关介绍
人工智能·ai
俊哥V4 小时前
每日 AI 研究简报 · 2026-09-14
人工智能·ai