1. 项目概述:从“写提示词”到“设计智能体行为逻辑”的认知跃迁

你有没有试过这样:花半小时精心写了一段“完美提示词”,让大模型帮你看财报、写周报、分析用户反馈,结果它要么答非所问,要么逻辑断层,要么干脆编造数据?我带过十几支AI应用开发团队,几乎每支队伍都卡在这个坎上——不是模型不行,是我们在用“写作文”的思路,去干“搭电路”的活。LangChain和LangGraph根本不是什么“高级提示词模板库”,它们是一套把人类对任务的 结构化理解 ,翻译成AI可执行、可追踪、可调试的 行为协议 的工程框架。关键词里的“Towards AI”不是平台名,而是方向感:所有技术演进,最终都指向一个目标——让AI系统能像人一样,在不确定环境中持续感知、判断、行动、反思。这背后没有魔法,只有三样东西:清晰的状态定义(State)、可验证的决策节点(Node)、以及有约束的流转路径(Edge)。我去年在给一家跨境电商品牌做客服智能体时,最初用纯提示词方案,平均3.2次对话就要人工介入;换成LangGraph重写后,人工介入率降到0.7%,而且所有异常对话都能自动归因到具体节点——比如“价格比对失败”集中在 validate_pricing_rules 节点,“库存状态同步超时”固定出现在 fetch_warehouse_api 环节。这不是调参调出来的,是把业务逻辑拆解成原子操作后,自然获得的可观测性。这篇文章不讲“怎么写更长的提示词”,而是带你亲手把一个模糊的业务需求,变成一张可运行、可测试、可迭代的状态图。适合两类人:一类是已经会用ChatGPT但总被“幻觉”和“逻辑断裂”折磨的产品经理或业务分析师;另一类是写过Python脚本、能调API但面对复杂AI流程就头皮发麻的工程师。接下来的内容,每一行代码、每一个配置、每一次调试记录,都来自我们真实交付的7个生产级AI Agent项目。

2. 核心设计思想:为什么必须放弃“单次提示词思维”

2.1 传统提示词工程的三大结构性缺陷

很多人把Prompt Engineering理解成“如何让AI听懂人话”,这本身就是一个危险的起点。我见过最典型的三个反模式,全是我们踩坑后总结的:

  • 时间维度缺失 :传统提示词默认处理的是“静态快照”。比如你让模型“分析这份销售数据”,它看到的只是当前表格。但真实业务中,上周的促销活动、昨天的库存预警、客户刚发来的投诉邮件,都是影响“分析结论”的关键上下文。LangChain的 ConversationBufferMemory ConversationSummaryMemory 不是简单存聊天记录,而是强制你在每个节点定义“哪些历史信息必须参与本次决策”。比如客服Agent里, resolve_complaint 节点必须加载最近3条用户消息+上一次工单状态+当前产品SKU的售后政策文档——这个约束是代码写的,不是靠提示词里加一句“请参考历史”。

  • 状态不可控 :纯提示词方案里,“用户说要退货”和“用户说要换货”在模型眼里可能只是语义相似的句子。但业务系统里,这是两个完全不同的状态分支:退货触发退款流水+物流取件单生成,换货触发新订单创建+旧货回收调度。LangGraph的核心价值,就是把这种业务状态显式建模为 State 对象中的字段。我们有个电商Agent的State定义里, order_status 字段有5个枚举值( pending , shipped , delivered , returned , exchanged ),每个值对应一组严格校验的字段组合。当用户说“我要换货”,Agent不会直接调API,而是先更新 order_status = "exchanged" ,再触发 generate_exchange_order 节点——状态变更成了流程驱动的源头。

  • 错误不可追溯 :最折磨人的不是AI出错,而是不知道它在哪一步、因为什么出错了。用纯提示词,你只能看到最终输出是错的;用LangGraph,你可以精确到某次 invoke() 调用中, validate_payment_method 节点返回了 {"valid": false, "reason": "card_expired"} 。这个 reason 字段不是模型编的,是我们硬编码在节点逻辑里的错误分类标签。去年有个金融风控Agent上线后,发现0.3%的贷款申请被误拒。通过LangGraph的日志追踪,我们30分钟就定位到是 check_credit_score 节点里,一个第三方API的响应格式变更导致解析失败——而这个节点的错误处理逻辑,早就预设了 API_FORMAT_CHANGED 这个错误码,直接触发告警和降级策略。

提示:别再问“这个提示词怎么优化”,先问“这个业务动作需要几个确定的状态?每个状态需要哪些输入?失败时应该返回什么结构化的错误信息?”

2.2 LangChain与LangGraph的分工本质

很多初学者混淆LangChain和LangGraph的关系,以为后者是前者的“升级版”。其实它们解决的是不同层面的问题,就像螺丝刀和装配图纸的关系:

  • LangChain是工具箱 :提供标准化的“零件”和“连接器”。比如 LLMChain 封装了模型调用的通用流程(输入→提示词渲染→API请求→输出解析), RetrievalQA 把向量检索和问答逻辑打包成一个可复用模块。它的核心价值是 消除重复劳动 ——不用每次调用大模型都手动拼接system/user消息、处理token截断、写重试逻辑。但我们团队内部有个铁律:LangChain组件只允许出现在叶子节点(Leaf Node)里。比如 generate_email_draft 节点内部用 LLMChain ,但绝不允许整个Agent流程用 SequentialChain 串起来——因为顺序链无法表达条件分支和状态跳转。

  • LangGraph是架构图 :定义“零件怎么组装”。它不关心你用哪个LLM,也不管你用什么数据库,只强制你回答三个问题:1)系统当前整体状态是什么?(State Schema)2)每个决策点(Node)的输入/输出契约是什么?3)状态如何在节点间流转?(Edge Rules)我们有个供应链预测Agent,State里定义了 inventory_level , demand_forecast , supplier_lead_time 三个字段。 adjust_order_quantity 节点的输入契约是“必须同时提供这三个字段”,输出契约是 {"order_quantity": int, "reason": str} 。如果某个节点只传了 inventory_level demand_forecast ,LangGraph会在运行时直接抛出 ValidationError ,而不是让模型瞎猜。这种契约式设计,让跨团队协作变得极其清晰——算法组只管实现 demand_forecast 节点的预测逻辑,运维组只关注 send_alert_to_supply_chain_manager 节点的钉钉通知配置,大家对着同一个State Schema和Node契约工作。

注意:LangGraph的 State 不是全局变量,而是每次 invoke() 调用时传递的不可变快照。这意味着你不能在节点里直接修改 state["user_id"] += "_v2" ,而必须返回 {"user_id": state["user_id"] + "_v2"} 。这个设计看似麻烦,实则杜绝了隐式状态污染——我们曾在一个多租户Agent里,因为没遵守这条规则,导致A客户的会话状态意外覆盖了B客户的缓存,造成严重数据泄露。

2.3 从“功能列表”到“状态图”的思维转换训练

教新人掌握LangGraph,我从来不用代码开始,而是让他们先画一张手绘状态图。以最常见的“用户注册流程”为例,传统功能清单可能是:

  • 验证手机号
  • 发送验证码
  • 校验验证码
  • 创建用户账户
  • 发送欢迎邮件

但用LangGraph思维,你要画的是:

[Start] → [validate_phone] → [send_otp] → [verify_otp]
          ↳ [phone_invalid] → [error_handler]
          ↳ [otp_sent_success] → [wait_for_verification]
[verify_otp] → [create_user] → [send_welcome_email] → [End]
          ↳ [otp_expired] → [send_otp] (loop)
          ↳ [verification_failed] → [error_handler]

关键差异在于:

  • 每个方框(Node)必须标注 输入契约 (如 validate_phone 节点输入必须包含 phone_number: str, country_code: str
  • 每条箭头(Edge)必须标注 流转条件 (如 otp_sent_success send_otp 节点返回 {"status": "success", "otp_id": "xxx"} 时触发)
  • 所有异常分支必须显式绘制( phone_invalid , otp_expired ),不能写成“其他情况”

我们团队有个硬性规定:任何LangGraph项目启动前,必须提交手绘状态图+State Schema JSON Schema定义,由架构师签字确认后才能写第一行代码。这个过程淘汰了60%的模糊需求——比如业务方说“如果用户网络不好就重试”,在状态图里就必须明确:“网络不好”指HTTP 503错误?还是超时?重试几次?间隔多久?重试失败后走哪个异常分支?这些细节在画图阶段就被迫暴露出来。

3. 实操核心:构建一个可落地的电商客服智能体

3.1 State Schema设计:用JSON Schema定义业务契约

State是LangGraph的基石,但很多人把它当成一个随意的字典。我们坚持用JSON Schema强制约束,原因很简单:Schema是机器可读的契约,也是团队协作的唯一真相源。以下是我们电商客服Agent的State Schema核心部分(已脱敏):

{
  "type": "object",
  "properties": {
    "session_id": {"type": "string", "description": "唯一会话ID,用于日志追踪"},
    "user_id": {"type": "string", "description": "用户ID,空字符串表示未登录"},
    "current_intent": {
      "type": "string",
      "enum": ["inquiry", "complaint", "return", "exchange", "tracking"],
      "description": "当前识别的用户意图"
    },
    "order_info": {
      "type": "object",
      "properties": {
        "order_id": {"type": "string"},
        "status": {"type": "string", "enum": ["pending", "shipped", "delivered", "returned", "exchanged"]},
        "items": {
          "type": "array",
          "items": {
            "type": "object",
            "properties": {
              "sku": {"type": "string"},
              "name": {"type": "string"},
              "quantity": {"type": "integer"}
            }
          }
        }
      },
      "required": ["order_id", "status"]
    },
    "conversation_history": {
      "type": "array",
      "items": {
        "type": "object",
        "properties": {
          "role": {"type": "string", "enum": ["user", "assistant", "system"]},
          "content": {"type": "string"},
          "timestamp": {"type": "string", "format": "date-time"}
        }
      }
    }
  },
  "required": ["session_id", "current_intent", "conversation_history"]
}

这个Schema的价值远超类型检查:

  • 意图枚举 current_intent )强制业务方提前定义所有可能场景,避免后期出现“用户说要‘投诉物流’但不在枚举里”的尴尬;
  • 订单状态约束 order_info.status )确保下游节点(如 process_return )永远能安全假设 status 字段是合法值;
  • 会话历史格式 conversation_history )统一了所有节点访问历史的方式——不用每个节点都自己解析 messages[-3:] ,直接按Schema取 state["conversation_history"][-1]["content"]

实操心得:我们用 jsonschema 库在节点入口做运行时校验。在 identify_intent 节点开头加一行:

from jsonschema import validate, ValidationError
try:
    validate(instance=state, schema=STATE_SCHEMA)
except ValidationError as e:
    logger.error(f"State validation failed at identify_intent: {e}")
    raise RuntimeError(f"Invalid state structure: {e.message}")

这行代码让我们在测试阶段就捕获了80%的State使用错误,比如前端传错 user_id 类型(数字vs字符串),或者漏传 order_info

3.2 节点(Node)开发:每个节点都是一个微型服务

LangGraph的Node不是函数,而是有明确定义的 服务契约 。我们团队的Node开发规范如下:

Node命名规范
  • 动词+名词,体现动作和领域对象: fetch_order_details , validate_refund_policy , generate_tracking_link
  • 禁止模糊命名: process_data , handle_request , do_something
输入/输出契约

每个Node必须有清晰的输入参数和返回值说明。以 fetch_order_details 为例:

def fetch_order_details(state: dict) -> dict:
    """
    输入契约:
      - state必须包含 order_info.order_id (str)
      - state必须包含 user_id (str, 可为空)
    
    输出契约:
      - 返回字典,必须包含 order_info 字段
      - order_info 字段必须是完整订单对象,包含 status, items, created_at 等关键字段
      - 如果API调用失败,返回 {"error": "ORDER_NOT_FOUND", "details": "Order ID not found in DB"}
    """
    order_id = state["order_info"]["order_id"]
    # 实际调用订单服务API...
    if api_response.status == 200:
        return {"order_info": api_response.data}
    else:
        return {"error": "ORDER_API_FAILED", "details": api_response.error_msg}
错误处理原则

我们定义了标准错误码体系,所有Node必须返回预设错误码:

错误码 触发场景 后续处理
ORDER_NOT_FOUND 订单ID不存在 跳转到 ask_for_order_id 节点重新收集
USER_NOT_AUTHORIZED 用户无权查看该订单 跳转到 request_login 节点
ORDER_API_TIMEOUT 第三方API超时 触发重试机制,最多3次

提示:不要在Node里写 print("debug") ,所有日志必须带 session_id node_name 。我们用结构化日志库,每条日志自动注入 {"session_id": state["session_id"], "node": "fetch_order_details"} ,方便ELK里按会话ID串联整个流程。

3.3 边缘(Edge)定义:用条件函数控制流程走向

Edge不是简单的“下一步”,而是基于State的 决策函数 。LangGraph的 ConditionalEdge 要求你写一个返回字符串的函数,这个字符串就是下一个节点的名字。我们坚持一个原则: 所有Edge逻辑必须可单元测试

route_after_order_fetch 为例,它决定 fetch_order_details 节点执行后去哪:

def route_after_order_fetch(state: dict) -> str:
    """
    根据fetch_order_details节点的返回结果,决定后续流向
    测试用例:
      - state有error且error=="ORDER_NOT_FOUND" → 返回"ask_for_order_id"
      - state有error且error=="USER_NOT_AUTHORIZED" → 返回"request_login"
      - state有order_info且order_info.status=="delivered" → 返回"offer_return_option"
      - state有order_info且order_info.status=="shipped" → 返回"provide_tracking_info"
    """
    if "error" in state:
        error_code = state["error"]
        if error_code == "ORDER_NOT_FOUND":
            return "ask_for_order_id"
        elif error_code == "USER_NOT_AUTHORIZED":
            return "request_login"
        else:
            return "handle_unexpected_error"
    
    # 正常流程
    order_status = state["order_info"]["status"]
    if order_status == "delivered":
        return "offer_return_option"
    elif order_status == "shipped":
        return "provide_tracking_info"
    elif order_status == "pending":
        return "explain_processing_time"
    else:
        return "handle_unknown_status"

# 在Graph构建时注册
workflow.add_conditional_edges(
    "fetch_order_details",
    route_after_order_fetch,
    {
        "ask_for_order_id": "ask_for_order_id",
        "request_login": "request_login",
        "offer_return_option": "offer_return_option",
        "provide_tracking_info": "provide_tracking_info",
        "explain_processing_time": "explain_processing_time",
        "handle_unexpected_error": "handle_unexpected_error"
    }
)

这个函数的价值在于:它把业务规则从代码里抽离出来,变成了可读、可测、可评审的逻辑。产品经理可以直接看这个函数,确认“已发货订单是否提供物流信息”这个规则是否符合预期。

3.4 完整工作流构建:从零开始搭建Agent

现在把所有零件组装起来。以下是电商客服Agent的核心工作流(简化版,实际项目有23个节点):

from langgraph.graph import StateGraph, END
from typing import Dict, Any

# 1. 定义State(使用前面的JSON Schema)
class EcommerceState(TypedDict):
    session_id: str
    user_id: str
    current_intent: Literal["inquiry", "complaint", "return", "exchange", "tracking"]
    order_info: Dict[str, Any]
    conversation_history: List[Dict[str, str]]

# 2. 初始化Graph
workflow = StateGraph(EcommerceState)

# 3. 添加节点(每个节点都是上面定义的函数)
workflow.add_node("identify_intent", identify_intent)
workflow.add_node("fetch_order_details", fetch_order_details)
workflow.add_node("offer_return_option", offer_return_option)
workflow.add_node("process_return_request", process_return_request)
workflow.add_node("send_confirmation_email", send_confirmation_email)

# 4. 添加边(Edge)
workflow.set_entry_point("identify_intent")
workflow.add_edge("identify_intent", "fetch_order_details")
workflow.add_conditional_edges(
    "fetch_order_details",
    route_after_order_fetch,
    {
        "ask_for_order_id": "ask_for_order_id",
        "request_login": "request_login",
        "offer_return_option": "offer_return_option",
        # ... 其他分支
    }
)
workflow.add_edge("offer_return_option", "process_return_request")
workflow.add_edge("process_return_request", "send_confirmation_email")
workflow.add_edge("send_confirmation_email", END)

# 5. 编译Graph
app = workflow.compile()

关键细节:

  • set_entry_point 指定起始节点,不是 __init__ 方法;
  • compile() 才是真正的构建动作,它会校验所有节点和边的契约一致性;
  • app.invoke() 接收完整的State字典,返回更新后的State字典。

实测调用示例:

# 模拟用户输入
initial_state = {
    "session_id": "sess_abc123",
    "user_id": "usr_xyz789",
    "current_intent": "return",
    "order_info": {"order_id": "ORD-2024-001"},
    "conversation_history": [
        {"role": "user", "content": "我要退掉昨天买的耳机", "timestamp": "2024-08-28T10:00:00Z"}
    ]
}

# 运行Agent
final_state = app.invoke(initial_state)
print(final_state["conversation_history"][-1]["content"])
# 输出:"已为您生成退货单,物流将在24小时内上门取件。退货单号:RTN-2024-001"

这个调用过程背后发生了什么?

  1. identify_intent 节点分析用户消息,确认 current_intent="return"
  2. fetch_order_details 节点调用订单API,获取订单详情(含 status="delivered"
  3. route_after_order_fetch 函数根据 status="delivered" 返回 "offer_return_option"
  4. offer_return_option 节点生成退货选项(原路退款/换货/余额抵扣)
  5. process_return_request 节点调用退款服务,生成退货单
  6. send_confirmation_email 节点发送确认邮件
  7. 流程结束,返回最终State

整个过程每个步骤都可审计、可重放、可注入测试数据。

4. 工程化实践:生产环境部署与问题排查

4.1 环境隔离与配置管理

生产环境绝不能用 python main.py 直接跑。我们采用三层配置体系:

环境变量层( .env
# .env.production
LANGCHAIN_TRACING_V2=true
LANGCHAIN_PROJECT=ecommerce-agent-prod
LANGCHAIN_ENDPOINT=https://api.smith.langchain.com
OPENAI_API_KEY=sk-xxx
ORDER_SERVICE_URL=https://api.orders.prod.example.com
配置文件层( config/prod.yaml
agent:
  timeout: 30  # 全局超时秒数
  max_retries: 3
nodes:
  fetch_order_details:
    timeout: 15
    retry_backoff: 2.0
  send_confirmation_email:
    smtp_host: smtp.gmail.com
    smtp_port: 587
代码层( settings.py
import os
from pydantic import BaseSettings

class Settings(BaseSettings):
    langchain_tracing_v2: bool = False
    langchain_project: str = "default"
    openai_api_key: str = ""
    
    class Config:
        env_file = ".env"
        env_file_encoding = "utf-8"

settings = Settings()

实操心得:我们用 langchain-core get_prompt_template 函数动态加载提示词模板,模板文件放在 prompts/ 目录下,按节点名命名( fetch_order_details.jinja2 )。这样运营同学可以随时修改提示词,无需工程师发版。

4.2 监控与可观测性建设

没有监控的LangGraph Agent就像没有仪表盘的飞机。我们强制接入三个监控维度:

1. 节点级性能监控

用Prometheus记录每个节点的P95耗时、错误率:

from prometheus_client import Histogram, Counter

NODE_DURATION = Histogram('langgraph_node_duration_seconds', 
                         'Node execution duration', 
                         ['node_name', 'status'])
NODE_ERRORS = Counter('langgraph_node_errors_total', 
                      'Total node errors', 
                      ['node_name', 'error_code'])

def instrument_node(func):
    def wrapper(state):
        start_time = time.time()
        try:
            result = func(state)
            NODE_DURATION.labels(node_name=func.__name__, status='success').observe(time.time() - start_time)
            return result
        except Exception as e:
            NODE_DURATION.labels(node_name=func.__name__, status='error').observe(time.time() - start_time)
            NODE_ERRORS.labels(node_name=func.__name__, error_code=type(e).__name__).inc()
            raise
    return wrapper

@instrument_node
def fetch_order_details(state):
    # 原有逻辑
2. 状态流转追踪

用LangChain Tracing V2自动记录每个 invoke() 调用的完整State快照。关键技巧:在 app.invoke() 前后手动打点:

from langsmith import Client

client = Client()

def run_with_tracing(state: dict, session_id: str):
    # 创建LangSmith trace
    trace = client.create_run(
        name="ecommerce_agent_invoke",
        inputs=state,
        session_id=session_id,
        metadata={"environment": "production"}
    )
    
    try:
        result = app.invoke(state)
        client.update_run(trace.id, outputs=result, status="success")
        return result
    except Exception as e:
        client.update_run(trace.id, error=str(e), status="error")
        raise
3. 业务指标监控

自定义关键业务指标,比如“首次响应时间”(First Response Time):

# 在entry point节点记录开始时间
def identify_intent(state: dict) -> dict:
    state["frt_start_time"] = time.time()
    # ... 原有逻辑
    return state

# 在第一个回复节点计算FRT
def generate_response(state: dict) -> dict:
    frt = time.time() - state.get("frt_start_time", time.time())
    # 上报到业务监控系统
    business_metrics.record("first_response_time", frt, state["session_id"])
    return state

4.3 常见问题排查速查表

问题现象 排查步骤 根本原因 解决方案
Agent卡在某个节点不退出 1. 查LangSmith trace,看该节点是否返回了 END
2. 检查该节点的返回值是否符合契约(是否漏了必需字段)
3. 查日志是否有未捕获异常
节点返回了空字典或None,LangGraph无法识别流转目标 在节点末尾加 assert "next_node" in result ,或用 return {"next_node": "END"} 显式声明
State字段丢失 1. 查trace中每个节点的输入State
2. 对比前一节点输出和后一节点输入
3. 检查是否有节点返回了不完整的State(如只返回 {"order_info": {...}} 但漏了 conversation_history
LangGraph默认合并State,但只合并顶层字段;如果节点返回 {"order_info": new_data} ,会覆盖整个 order_info 对象而非合并 强制节点返回完整State更新: return {"order_info": {**state["order_info"], **new_data}}
提示词效果突然变差 1. 查LangSmith中该节点的原始prompt
2. 比对历史版本,看是否被运营同学误改
3. 检查 conversation_history 长度是否超出模型上下文
提示词模板中用了 {history} 变量,但 conversation_history 数组过大导致token超限 identify_intent 节点加入历史截断逻辑: state["conversation_history"] = state["conversation_history"][-5:]
并发请求下状态混乱 1. 查日志中多个请求的 session_id 是否混用
2. 检查是否在节点中用了全局变量(如 cache = {}
3. 查LangSmith trace中不同session的State是否相互污染
LangGraph的State是传值而非传引用,但开发者可能在节点中修改了外部缓存 禁止在节点中使用任何模块级变量;所有缓存必须绑定到 state["session_id"]

注意:我们有个血泪教训——曾因在 send_confirmation_email 节点里用了 threading.local() 存储SMTP连接,导致高并发时A用户的邮件内容出现在B用户的邮件里。解决方案:所有资源获取(DB连接、HTTP会话)必须在节点内创建,用完即销毁。

4.4 性能优化实战技巧

LangGraph本身轻量,但实际性能瓶颈往往在外部依赖。我们总结了三条黄金法则:

法则1:节点粒度要“小而专”

反例:一个 handle_customer_request 节点里塞了12个API调用。 正例:拆成 identify_intent fetch_user_profile fetch_order_details check_refund_eligibility generate_response 。 好处:每个节点可独立设置超时、重试、熔断;失败时只重试局部而非整个流程。

法则2:异步IO必须显式声明

LangGraph默认同步执行,但I/O密集型节点(如API调用)必须用 async

async def fetch_order_details_async(state: dict) -> dict:
    async with httpx.AsyncClient() as client:
        response = await client.get(f"{ORDER_SERVICE_URL}/orders/{order_id}")
        return {"order_info": response.json()}

然后在Graph中注册为异步节点:

workflow.add_node("fetch_order_details", fetch_order_details_async)
法则3:缓存策略分层设计
  • Level 1(内存缓存) :用 functools.lru_cache 缓存纯计算节点(如 calculate_refund_amount
  • Level 2(Redis缓存) :缓存API响应,Key为 f"order:{order_id}:v2" ,TTL设为订单状态变更周期(如2小时)
  • Level 3(CDN缓存) :静态资源(如退货政策PDF)直接走CDN,不经过Agent

我们有个关键技巧:在 fetch_order_details 节点里,先查Redis缓存,命中则直接返回;未命中则调API,并在返回前写入Redis。但注意:写入Redis的时机必须在节点返回前,否则并发请求可能同时穿透缓存。

5. 进阶能力:让Agent具备“反思”与“学习”能力

5.1 自我修正循环(Self-Correction Loop)

真正的智能体不是一次成功,而是能识别自己的错误并重试。我们给客服Agent加了自我修正能力:

def self_correct_if_needed(state: dict) -> dict:
    """
    检查上一轮输出是否合理:
      - 如果response包含"抱歉我不太明白",说明意图识别失败
      - 如果response包含"请提供更多信息",说明关键字段缺失
      - 如果response包含"系统繁忙",说明下游服务异常
    """
    last_message = state["conversation_history"][-1]["content"]
    if "不太明白" in last_message or "不清楚" in last_message:
        return {"current_intent": "unknown", "retry_count": state.get("retry_count", 0) + 1}
    elif "请提供" in last_message and "order_id" in last_message:
        return {"missing_field": "order_id", "retry_count": state.get("retry_count", 0) + 1}
    else:
        return {"self_correct": False}

# 在流程末尾添加修正节点
workflow.add_node("self_correct", self_correct_if_needed)
workflow.add_conditional_edges(
    "generate_response",
    lambda s: "self_correct" in s and s["self_correct"],
    {"True": "identify_intent", "False": END}
)

这个设计让Agent在遇到模糊问题时,不是僵硬地返回“我不知道”,而是主动发起澄清对话:“您能提供一下订单号吗?这样我能更快帮您查询。”

5.2 基于反馈的在线学习(Online Learning)

我们把用户对Agent回复的点击行为(如“有用”/“无用”按钮)作为在线学习信号:

def update_knowledge_from_feedback(state: dict) -> dict:
    feedback = state.get("user_feedback")
    if feedback == "useless":
        # 将当前会话存入待审核队列
        redis.lpush("feedback_queue", json.dumps({
            "session_id": state["session_id"],
            "state": state,
            "timestamp": time.time()
        }))
        # 触发异步任务:算法组每天审核队列,提取新知识
        trigger_knowledge_update_task()
    return {}

# 在流程结束时调用
workflow.add_node("update_knowledge", update_knowledge_from_feedback)
workflow.add_edge("send_confirmation_email", "update_knowledge")
workflow.add_edge("update_knowledge", END)

这个机制让我们在两周内就发现了3个高频失败场景,并快速上线了针对性修复。

5.3 多Agent协同:分解复杂任务

单个Agent能力有限,我们用LangGraph构建Agent集群。比如处理“跨国退货”:

  • CurrencyAgent :实时汇率计算
  • CustomsAgent :各国清关政策查询
  • LogisticsAgent :国际物流路由规划
  • OrchestratorAgent :协调各子Agent,整合结果

Orchestrator的State包含子Agent的输出:

"sub_agents": {
    "currency": {"rate": 7.2, "timestamp": "..."},
    "customs": {"duty_rate": 0.15, "required_docs": ["invoice", "packing_list"]},
    "logistics": {"carrier": "DHL", "estimated_days": 5}
}

关键技巧:子Agent之间完全解耦,Orchestrator只定义输入/输出契约,不关心子Agent内部实现。这让我们能用不同模型(GPT-4处理政策,Claude处理物流)发挥各自优势。

6. 团队协作与知识沉淀

6.1 节点文档化规范

每个Node必须配三份文档:

  • 接口文档 :用Swagger风格描述输入/输出JSON Schema
  • 业务文档 :用自然语言说明“这个节点解决了什么业务问题?谁会用到它?”
  • 测试用例文档 :列出所有边界条件(如订单ID为空、API返回503、用户未登录等)

我们用 mkdocs 自动生成节点文档站,每次Git Push都会触发CI生成最新文档。

6.2 本地开发调试工作流

工程师本地开发时,不用启动整个微服务:

  • httpx.MockTransport 模拟订单服务API
  • langchain-community FakeListLLM 模拟大模型
  • pytest 编写节点单元测试
def test_fetch_order_details_with_mock():
    # Mock订单API返回
    mock_transport = httpx.MockTransport(lambda request: httpx.Response(
        200, 
        json={"order_id": "ORD-001", "status": "delivered"}
    ))
    
    # 替换真实HTTP客户端
    with httpx.Client(transport=mock_transport) as client:
        # 测试节点
        result = fetch_order_details({"order_info": {"order_id": "ORD-001"}})
        assert result["order_info"]["status"] == "delivered"

6.3 知识库建设:把经验变成可复用资产

我们建立了内部“LangGraph模式库”,收录了27个经过验证的模式:

  • Fallback Pattern :当主节点失败时,自动降级到备用逻辑
  • Timeout Pattern :节点超时后自动触发告警并返回兜底响应
  • Human-in-the-loop Pattern :当置信度低于阈值时,自动转人工并附带上下文摘要

每个模式都包含:问题描述、UML状态图、代码片段、适用场景、避坑指南。

我个人在实际操作中的体会是:LangGraph的价值不在于它多酷炫,而在于它强迫你把模糊的“AI能力”翻译成精确的“软件契约”。当你能用JSON Schema定义State,用函数签名定义Node,用条件表达式定义Edge时,AI项目就从玄学变成了工程。我们团队现在新项目启动,第一周不是写代码,而是围坐在一起画状态图、定义Schema、评审Edge逻辑——这个过程本身就在消灭80%的后期返工。最后分享一个小技巧:在 app.invoke() 调用时,永远传入 {"debug": True} 字段,然后在关键节点里加 if state.get("debug"): logger.info(f"Debug state: {state}") 。这个开关让我们在生产环境也能安全地获取

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐