LangGraph工程化实战:构建可调试、可审计、高并发AI Agent
简介:这是一份面向AI应用开发工程师的系统性AI Agent实战学习资源,聚焦大模型智能体从理论到落地的全链路能力构建,覆盖LangChain、LangGraph、Coze、Dify等主流框架及RAG、提示词工程、企业级部署与微调等核心模块,解决开发者在智能体项目中缺乏完整路径、工程经验与面试准备的痛点。资源包共2000个文件,以1588张技术示意图(png/jpeg/svg)直观呈现架构流程与界面操作,辅以127个可运行Python脚本、83篇结构化Markdown笔记、13段关键操作MP4视频及7个Excel数据集,总大小513.49MB,内容组织按学习阶段分层,便于循序渐进掌握。目前已有222人下载学习。读者可直接复用金融投研助手、医疗问诊系统、电商客服智能体等5个企业级实战项目的完整代码、配置文件与调试日志;获取涵盖算法推导、系统设计、故障排查的高频面试题库及参考答案;并获得RAG文档预处理、LoRA微调参数、LangGraph状态机定义等精准到配置项与版本兼容性的实操细节。
1. 这不是“搭积木”:2026年AI Agent速成指南,专治「学了LangChain却写不出能跑通的智能体」
你花三天啃完LangChain文档,照着官方示例跑通了OpenAIFunctionsAgent,兴奋地把agent.invoke({"input": "查下今天北京天气"})发进Jupyter——结果返回一串JSON格式错误;你又换LangGraph重写,画出带StateGraph和add_node的流程图,本地调试时节点死在tool_executor里,日志只报TypeError: 'NoneType' object is not callable;更常见的是:项目上线后QPS刚过5,API就开始超时,监控显示90%耗时卡在LLM调用等待队列。这不是你代码写得差,而是2026年真实生产环境里的AI Agent开发,早就不靠“调通一个demo”来定义成功。本指南不讲抽象概念,不堆API列表,只聚焦三件事:怎么让Agent在真实业务流里扛住并发、怎么用LangGraph把状态流转变成可审计的确定性行为、怎么把面试官问“你这个Agent失败了怎么定位”的问题,变成你简历里可演示的SOP。适合两类人:一是已会Python+FastAPI但被Agent工程化卡住的中级开发者,二是正从传统后端/数据工程转向大模型应用开发的转岗工程师。所有内容均基于2024–2025年国内一线AI中台团队落地的17个Agent项目复盘,含3个已上线金融/政务场景的完整链路。
2. 从零启动:用LangChain+LangGraph构建第一个可调试、可追踪的Agent
2.1 为什么必须放弃initialize_agent()?选型依据与架构分层
LangChain官方文档里那个initialize_agent(..., agent_type="openai-tools"),是教学友好型陷阱。它把工具注册、提示词组装、LLM调用、工具执行全封装进黑匣子,导致两个致命问题:一是无法在工具执行前注入业务校验逻辑(比如检查用户是否有权限调用“转账”工具),二是当工具返回异常时,错误堆栈直接消失在AgentExecutor内部,你只能看到AgentExecutionError而不知具体哪行Python代码崩了。2026年主流做法是显式分层:
- Orchestration Layer(编排层):用LangGraph的
StateGraph定义状态机,每个节点对应一个明确职责(如validate_input、route_to_tool、format_response); - Tool Layer(工具层):所有工具函数必须带类型注解、输入校验、重试策略,且返回结构统一为
ToolResult(success: bool, data: dict, error: str); - LLM Abstraction Layer(LLM抽象层):不直接调用
ChatOpenAI,而是封装LLMRouter类,内置缓存、降级开关、token用量统计钩子。
这种分层不是为了炫技,而是为后续加监控埋点。比如你在route_to_tool节点里加一行logger.info(f"Routing to {tool_name} for user_id={state['user_id']}"),就能在ELK里直接查“某用户连续5次调用失败是否都路由到同一工具”。
2.2 最小可运行Agent:57行代码跑通带状态追踪的循环
以下代码是2026年新入职工程师第一天必须手敲的“Hello World”,它不依赖任何外部API,所有工具用内存模拟,但具备生产环境必需的三大能力:状态可序列化、节点可单独测试、失败可回溯。
# agent_core.py from typing import TypedDict, Annotated, Sequence import operator from langgraph.graph import StateGraph, END from langgraph.checkpoint.memory import MemorySaver class AgentState(TypedDict): input: str user_id: str tool_calls: Annotated[Sequence[dict], operator.add] tool_results: Annotated[Sequence[dict], operator.add] retry_count: int final_answer: str def validate_input(state: AgentState) -> AgentState: if not state["input"].strip(): raise ValueError("Input cannot be empty") if len(state["input"]) > 500: raise ValueError("Input too long, max 500 chars") return {"retry_count": 0} def route_to_tool(state: AgentState) -> str: # 简单路由:含"天气"走weather,含"股票"走stock if "天气" in state["input"]: return "weather_tool" elif "股票" in state["input"]: return "stock_tool" else: return "default_response" def weather_tool(state: AgentState) -> AgentState: # 模拟调用天气API,实际应替换为requests.post(...) return { "tool_results": [{"tool": "weather", "data": {"city": "北京", "temp": "22°C", "condition": "晴"}}] } def stock_tool(state: AgentState) -> AgentState: return { "tool_results": [{"tool": "stock", "data": {"symbol": "AAPL", "price": 182.34}}] } def default_response(state: AgentState) -> AgentState: return {"final_answer": "我不理解您的请求,请用‘查天气’或‘查股票’开头"} def format_response(state: AgentState) -> AgentState: if state.get("tool_results"): result = state["tool_results"][-1] if result["tool"] == "weather": return {"final_answer": f"北京天气:{result['data']['temp']},{result['data']['condition']}"} elif result["tool"] == "stock": return {"final_answer": f"{result['data']['symbol']}当前股价:${result['data']['price']:.2f}"} return {"final_answer": "服务暂时不可用"} # 构建图 workflow = StateGraph(AgentState) workflow.add_node("validate_input", validate_input) workflow.add_node("route_to_tool", route_to_tool) workflow.add_node("weather_tool", weather_tool) workflow.add_node("stock_tool", stock_tool) workflow.add_node("default_response", default_response) workflow.add_node("format_response", format_response) workflow.set_entry_point("validate_input") workflow.add_edge("validate_input", "route_to_tool") workflow.add_conditional_edges( "route_to_tool", lambda x: x, { "weather_tool": "weather_tool", "stock_tool": "stock_tool", "default_response": "default_response" } ) workflow.add_edge("weather_tool", "format_response") workflow.add_edge("stock_tool", "format_response") workflow.add_edge("default_response", "format_response") workflow.add_edge("format_response", END) # 启用内存检查点,支持断点续跑 app = workflow.compile(checkpointer=MemorySaver())关键参数说明:
Annotated[Sequence[dict], operator.add]:这是LangGraph 0.1.0+强制要求的状态字段类型,operator.add表示该字段在节点间自动累加(如多次调用工具,tool_results会合并成列表);checkpointer=MemorySaver():不是可选项,是调试刚需。它让每次app.invoke()调用后,状态快照存入内存,你可用app.get_state(config)随时查看中间值;lambda x: x作为条件边函数:看似冗余,实则是为后续扩展留口——当你要根据LLM输出动态路由时,这里换成llm_router.invoke(state)即可,无需改图结构。
2.3 本地调试三板斧:用get_state()、stream()、interrupt()定位每一毫秒
光有代码不够,得知道怎么“看”它在跑什么。LangGraph的调试能力远超旧版LangChain,核心是三个方法:
第一板斧:get_state()查快照
config = {"configurable": {"thread_id": "test-001"}} # 先触发一次调用 app.invoke({"input": "查下北京天气", "user_id": "u123"}, config=config) # 再查状态,看到完整流转路径 state = app.get_state(config) print(f"当前节点: {state.values.get('current_node')}") print(f"工具结果: {state.values.get('tool_results')}") # 输出: 当前节点: format_response, 工具结果: [{'tool': 'weather', ...}]第二板斧:stream()看实时流
# 不用等整个流程结束,边跑边看 for output in app.stream({"input": "查股票AAPL", "user_id": "u123"}, config): print(f"节点 {list(output.keys())[0]} 输出: {list(output.values())[0]}") # 输出: # 节点 validate_input 输出: {'retry_count': 0} # 节点 route_to_tool 输出: stock_tool # 节点 stock_tool 输出: {'tool_results': [{'tool': 'stock', ...}]} # 节点 format_response 输出: {'final_answer': 'AAPL当前股价:$182.34'}第三板斧:interrupt()插桩断点
# 在weather_tool节点后中断,手动检查状态再继续 app.invoke( {"input": "查天气", "user_id": "u123"}, config={"configurable": {"thread_id": "debug-001", "interrupt_after": ["weather_tool"]}} ) # 此时流程停在weather_tool后,调用get_state()可修改state再resume state = app.get_state({"configurable": {"thread_id": "debug-001"}}) state.values["tool_results"][0]["data"]["temp"] = "25°C" # 强制改结果 app.update_state({"configurable": {"thread_id": "debug-001"}}, state.values) app.invoke(None, config={"configurable": {"thread_id": "debug-001"}}) # resume这三招组合,让你彻底告别print()调试。尤其interrupt_after,是排查“为什么工具返回了但没走到format_response”的终极武器——它能证明问题出在条件边逻辑,而非工具本身。
3. 生产就绪:让Agent扛住并发、可审计、能降级
3.1 并发瓶颈在哪?用async+ThreadPoolExecutor榨干CPU
LangGraph默认是同步执行,当10个请求同时进来,weather_tool节点会排队执行,哪怕它只是个内存计算。真实场景中,工具常涉及HTTP调用(天气API)、数据库查询(用户余额)、文件IO(读取PDF),这些I/O操作必须异步化。但注意:不是所有工具都适合async。比如调用requests.get()必须用asyncio.to_thread()包一层,而纯Python计算(如字符串解析)用concurrent.futures.ThreadPoolExecutor更高效。
# tools/weather.py import asyncio import aiohttp from concurrent.futures import ThreadPoolExecutor # 方案A:I/O密集型工具用aiohttp(推荐) async def async_weather_api(city: str) -> dict: async with aiohttp.ClientSession() as session: async with session.get(f"https://api.example.com/weather?city={city}") as resp: return await resp.json() # 方案B:CPU密集型工具用线程池(避免asyncio事件循环阻塞) def cpu_intensive_parse(raw_text: str) -> dict: # 模拟NLP解析,耗CPU import time; time.sleep(0.1) # 占用CPU 100ms return {"parsed": raw_text[:100]} # 在LangGraph节点中调用 async def weather_tool(state: AgentState) -> AgentState: # I/O工具:直接await weather_data = await async_weather_api("北京") # CPU工具:用线程池 with ThreadPoolExecutor(max_workers=4) as executor: loop = asyncio.get_event_loop() parsed = await loop.run_in_executor(executor, cpu_intensive_parse, str(weather_data)) return {"tool_results": [{"tool": "weather", "data": {**weather_data, **parsed}}]}参数调优经验:
ThreadPoolExecutor(max_workers=4):不是越大越好。经压测,当worker数超过CPU核数×2,线程切换开销反超收益;aiohttp连接池必须显式配置:aiohttp.TCPConnector(limit=100, limit_per_host=20),否则默认100连接会被瞬间打满;- 所有异步工具函数必须加
@traceable装饰器(用LangSmith),否则分布式追踪里看不到工具耗时。
3.2 可审计性:给每个Agent调用打上“数字指纹”
政务/金融类项目强制要求:任何Agent输出必须能追溯到原始输入、所用模型版本、工具调用链、人工审核记录。LangGraph本身不提供审计能力,需在State中嵌入audit_log字段,并用app.add_node()注入审计节点。
def audit_log(state: AgentState) -> AgentState: import uuid, datetime audit_id = str(uuid.uuid4()) log_entry = { "audit_id": audit_id, "timestamp": datetime.datetime.now().isoformat(), "user_id": state["user_id"], "input": state["input"], "model_used": "qwen2.5-72b", "tools_called": [r["tool"] for r in state.get("tool_results", [])], "status": "success" if state.get("final_answer") else "failed" } # 写入审计数据库(此处简化为print,实际用SQLAlchemy异步写) print(f"[AUDIT] {log_entry}") return {"audit_id": audit_id} # 插入到图中 workflow.add_node("audit_log", audit_log) workflow.add_edge("format_response", "audit_log") workflow.add_edge("audit_log", END)审计字段设计原则:
audit_id必须全局唯一且可索引,建议用ulid(比UUID更有序,利于时间范围查询);model_used不能写死,应从LLMRouter的get_current_model()动态获取,确保模型热切换时审计日志同步更新;tools_called存工具名列表,而非完整调用栈——后者体积太大,且tool_results里已有详细数据,审计库只需存轻量索引。
3.3 降级策略:当LLM挂了,Agent还能回答“我不知道”
LLM服务不可用是常态。2026年成熟方案是三级降级:
- 一级降级(毫秒级):LLM调用超时(<2s),返回预设兜底话术;
- 二级降级(秒级):LLM连续3次超时,自动切换备用模型(如从Qwen切到GLM);
- 三级降级(分钟级):备用模型也失效,启用规则引擎(Rule Engine)——用正则+关键词匹配生成答案。
# llm_router.py import asyncio from langchain_community.chat_models import ChatOllama from langchain_core.messages import HumanMessage class LLMRouter: def __init__(self): self.primary = ChatOllama(model="qwen2.5:72b", timeout=2.0) self.backup = ChatOllama(model="glm4:latest", timeout=3.0) self.rule_engine = RuleEngine() # 见下文 async def invoke(self, messages: list) -> str: try: # 一级:主模型超时即降级 return await asyncio.wait_for( self.primary.ainvoke(messages), timeout=2.0 ) except asyncio.TimeoutError: try: # 二级:切备用模型 return await asyncio.wait_for( self.backup.ainvoke(messages), timeout=3.0 ) except asyncio.TimeoutError: # 三级:规则引擎兜底 return self.rule_engine.fallback(messages[-1].content) class RuleEngine: def fallback(self, input_text: str) -> str: if "天气" in input_text: return "我暂时无法获取实时天气,请稍后再试。" elif "股票" in input_text: return "股市数据暂不可用,建议通过证券APP查询。" else: return "当前服务繁忙,请描述更具体的问题。"血泪经验:
asyncio.wait_for()的timeout必须小于HTTP客户端的timeout(如aiohttp的timeout.total),否则外层超时后内层还在跑,浪费资源;- 规则引擎的
fallback方法必须是纯函数、无IO、无网络调用,确保在LLM全挂时仍能毫秒响应;- 降级开关要可动态配置,用Redis存
llm:degrade:enabled,运维可随时SET llm:degrade:enabled 1强制降级。
4. 避坑指南:LangChain+LangGraph项目里最常踩的5个深坑
4.1 现象:StateGraph编译时报ValidationError: field required,但所有字段都写了
原因:LangGraph 0.1.x要求State类中每个字段必须有默认值或类型注解中的Optional,即使你声明了str,也必须写input: str = ""。旧版LangChain允许input: str,但LangGraph会严格校验Pydantic v2的strict模式。
解决:将AgentState改为TypedDict并补全默认值:
from typing import TypedDict, Annotated, Sequence, Optional import operator class AgentState(TypedDict): input: str user_id: str tool_calls: Annotated[Sequence[dict], operator.add] tool_results: Annotated[Sequence[dict], operator.add] retry_count: int final_answer: str # ✅ 必须加这一行,否则编译失败 audit_id: Optional[str] # 或 audit_id: str = ""4.2 现象:本地stream()调试正常,部署到K8s后stream()返回空迭代器
原因:K8s Pod默认关闭stdout行缓冲,stream()依赖print()逐行输出,当输出未换行或缓冲未刷新,前端收不到数据。
解决:在stream()循环中强制刷新:
import sys for output in app.stream(input_data, config): print(json.dumps(output), flush=True) # ✅ 加flush=True sys.stdout.flush() # ✅ 双保险同时在Dockerfile中加
ENV PYTHONUNBUFFERED=1,确保Python不缓存stdout。
4.3 现象:tool_executor节点报TypeError: 'NoneType' object is not callable
原因:工具函数返回None,而LangGraph的tool_executor期望返回dict或BaseModel。常见于工具里用了return但没写返回值,或异常分支漏了return。
解决:所有工具函数末尾加防御性返回:
def weather_tool(state: AgentState) -> AgentState: try: data = call_weather_api(state["input"]) return {"tool_results": [{"tool": "weather", "data": data}]} except Exception as e: # ✅ 必须返回,不能只raise return {"tool_results": [{"tool": "weather", "error": str(e)}]}4.4 现象:MemorySaver在多进程下状态丢失,get_state()查不到历史
原因:MemorySaver是纯内存实现,K8s多副本或Gunicorn多Worker时,每个进程有独立内存,状态不共享。
解决:生产必须换PostgresSaver或RedisSaver:
from langgraph.checkpoint.postgres import PostgresSaver import psycopg # 初始化Postgres连接池 conn = psycopg.connect("host=localhost dbname=langgraph user=pg password=123") checkpointer = PostgresSaver(conn) app = workflow.compile(checkpointer=checkpointer)注意:Postgres需提前建表,执行
checkpointer.setup(),否则首次invoke会报错。
4.5 现象:用@traceable装饰工具函数,LangSmith里看不到工具耗时
原因:@traceable必须作用于异步函数,且调用链中不能有同步阻塞操作(如time.sleep())。若工具里混用time.sleep(),整个trace会被截断。
解决:
- 所有工具函数声明为
async def; - 同步阻塞操作用
await asyncio.to_thread(time.sleep, 0.1); @traceable放在async def上方,且run_type="tool":
from langsmith import traceable @traceable(run_type="tool") async def weather_tool(state: AgentState) -> AgentState: await asyncio.to_thread(time.sleep, 0.1) # ✅ 正确 # ...5. 面试通关:用一个真实项目拆解“智能体开发工程师”考什么
5.1 面试官真正在意的不是你会不会写add_node,而是你如何定义“一个好Agent”
2026年大厂AI岗位面试已淘汰“手写Agent代码”环节,转为场景化系统设计题。例如:“请设计一个销售陪练Agent,帮助新人销售模拟客户异议处理。要求:1)能根据客户画像(年龄/行业/历史投诉)动态调整话术;2)每次模拟后生成改进建议;3)支持人工教练介入修正”。这类题考察三层能力:
| 能力层 | 面试官想听的答案要点 | 你该避开的雷区 |
|---|---|---|
| 业务理解层 | “客户画像应作为State字段传入,话术模板存在向量库,用RAG检索匹配度>0.85的案例” | 只说“用LLM生成话术”,不提数据源和匹配逻辑 |
| 工程鲁棒层 | “异议检测用规则引擎初筛(关键词+正则),再送LLM精标,避免LLM误判‘价格贵’为投诉” | 声称“全靠LLM识别”,无兜底方案 |
| 可观测层 | “每个模拟session生成audit_id,关联到CRM工单号,教练介入时自动拉取历史3次对话” | 不提审计、不提与现有系统集成 |
玄学提醒:当面试官问“如果客户说‘你们产品太贵’,Agent怎么回应”,千万别答“让LLM自由发挥”。正确答案是:“先查该客户所在行业的平均客单价,若报价低于均值10%,返回‘我们提供行业性价比最优方案’;若高于均值,则触发‘价值澄清’子流程,用3个客户案例证明ROI”。
5.2 实战项目:用FastAPI+LangGraph搭建“销售陪练Agent”最小可行版
我们用200行代码实现上述面试题的核心链路,重点展示如何把业务规则硬编码进StateGraph,而非依赖LLM幻觉。
# sales_coach.py from fastapi import FastAPI, HTTPException from pydantic import BaseModel from langgraph.graph import StateGraph, END from typing import Dict, List, Optional class SalesState(BaseModel): customer_profile: Dict # {"age": 35, "industry": "电商", "complaints": 2} current_dialogue: List[Dict] # [{"role": "user", "content": "太贵了"}] suggested_response: str improvement_tips: List[str] audit_id: str def load_industry_benchmarks(industry: str) -> Dict: # 真实项目应从DB查,此处硬编码 benchmarks = { "电商": {"avg_price": 8000, "roi_months": 3}, "制造": {"avg_price": 12000, "roi_months": 6} } return benchmarks.get(industry, {"avg_price": 10000, "roi_months": 4}) def detect_complaint(state: SalesState) -> SalesState: last_user_msg = state.current_dialogue[-1]["content"] # ✅ 用规则引擎初筛,非LLM if any(word in last_user_msg for word in ["贵", "贵了", "太贵"]): benchmark = load_industry_benchmarks(state.customer_profile["industry"]) if state.customer_profile.get("budget", 0) < benchmark["avg_price"] * 0.9: state.suggested_response = f"我们理解预算关切。行业平均投入是{benchmark['avg_price']}元,您当前方案已节省{int((benchmark['avg_price']-state.customer_profile.get('budget',0))/benchmark['avg_price']*100)}%。" else: state.suggested_response = f"投资回报周期约{benchmark['roi_months']}个月,这是电商客户平均数据。" else: state.suggested_response = "请具体说明您的顾虑,我会针对性解答。" return state def generate_tips(state: SalesState) -> SalesState: # ✅ 基于对话轮次生成建议,非LLM生成 turns = len(state.current_dialogue) if turns <= 2: state.improvement_tips = ["保持开放提问,避免过早解释"] elif turns <= 4: state.improvement_tips = ["尝试用客户行业案例佐证"] else: state.improvement_tips = ["总结客户核心诉求,确认理解一致"] return state # 构建图 workflow = StateGraph(SalesState) workflow.add_node("detect_complaint", detect_complaint) workflow.add_node("generate_tips", generate_tips) workflow.set_entry_point("detect_complaint") workflow.add_edge("detect_complaint", "generate_tips") workflow.add_edge("generate_tips", END) app = workflow.compile() # FastAPI接口 fastapi_app = FastAPI() class SalesRequest(BaseModel): customer_profile: dict dialogue_history: list @fastapi_app.post("/sales-coach") async def coach_sales(request: SalesRequest): try: # ✅ 自动生成audit_id,关联CRM import uuid audit_id = f"SC-{uuid.uuid4().hex[:8]}" state = SalesState( customer_profile=request.customer_profile, current_dialogue=request.dialogue_history, suggested_response="", improvement_tips=[], audit_id=audit_id ) result = app.invoke(state) return { "audit_id": result.audit_id, "suggested_response": result.suggested_response, "improvement_tips": result.improvement_tips } except Exception as e: raise HTTPException(status_code=500, detail=str(e))部署验证命令(确保你真能跑通):
# 启动服务 uvicorn sales_coach:fastapi_app --reload # 发送测试请求 curl -X POST "http://localhost:8000/sales-coach" \ -H "Content-Type: application/json" \ -d '{ "customer_profile": {"age": 35, "industry": "电商", "budget": 6000}, "dialogue_history": [{"role": "user", "content": "你们产品太贵了"}] }' # 预期返回: # {"audit_id":"SC-abc123","suggested_response":"我们理解预算关切。行业平均投入是8000元...","improvement_tips":["保持开放提问..."]}
5.3 面试题库实战:3道高频题的标准答案结构
题1:Agent失败时如何定位?
我的回答结构:
- 第一层查审计ID:从用户反馈拿到
audit_id,在ELK查该ID的完整日志链;- 第二层看状态快照:用
app.get_state({"configurable": {"thread_id": "xxx"}})看失败前最后状态,确认是tool_results为空还是final_answer未生成;- 第三层复现节点:把
tool_results字段复制出来,单独调用weather_tool({"tool_results": [...]}),隔离测试工具函数。
✅ 关键点:不提“看LLM返回”,因为LLM返回是黑盒,要定位到具体节点。
题2:如何评估Agent效果?
我的回答结构:
- 线上指标:
success_rate(最终answer非空且含有效信息)、avg_latency(从invoke到END的P95)、tool_error_rate(工具层异常占比);- 线下指标:用
agentdojo跑回归测试,对100个标准case打分(如“天气查询”必须含温度+天气状况);- 业务指标:销售陪练场景下,看使用Agent的销售新人“首次成交周期”是否缩短15%。
✅ 关键点:必须区分技术指标和业务指标,且给出具体数字。
题3:LangChain和LangGraph选哪个?
我的回答结构:
- LangChain适合:单次调用、无状态、工具少(<3个)的简单场景,如客服机器人问答;
- LangGraph必须用:需要状态管理(如多轮对话)、条件分支(如根据用户等级路由不同工具)、人工干预(如审核节点)、长流程(如贷款审批);
- 2026年趋势:新项目100%用LangGraph,LangChain仅用于胶水层(如封装工具函数)。
✅ 关键点:用“必须用”代替“推荐用”,体现技术判断力。
6. 终极技巧:用LangGraph的interrupt_before实现“人工审核”工作流
这是我在某政务项目里亲手落地的技巧,解决了“AI生成内容必须人工签字才能发布”的强合规需求。核心思路:不让人工审核成为流程瓶颈,而是把它变成图中的一个可选节点,且支持跳过。
# 审核节点定义 def human_review(state: AgentState) -> AgentState: # 生成待审核内容摘要 summary = f"用户{state['user_id']}请求:{state['input'][:50]}...,AI建议:{state['final_answer'][:100]}" # ✅ 关键:不阻塞,只发通知,状态机继续跑 send_review_notification(summary, state["audit_id"]) return {"review_status": "pending", "review_summary": summary} # 在图中插入审核节点 workflow.add_node("human_review", human_review) workflow.add_conditional_edges( "format_response", lambda x: "review_required" if x["user_id"].startswith("gov_") else "no_review", { "review_required": "human_review", "no_review": END } ) # ✅ 关键:审核通过后,用interrupt_resume恢复 @app.post("/review/approve/{audit_id}") async def approve_review(audit_id: str): # 人工审核通过,恢复流程 config = {"configurable": {"thread_id": audit_id}} state = app.get_state(config) state.values["review_status"] = "approved" app.update_state(config, state.values) # resume到END节点 app.invoke(None, config=config) return {"status": "approved"}为什么这招比“加一个等待审核的节点”高明?
- 它让
format_response之后的流程立即返回“已提交审核”,用户不用等;- 审核员在后台系统看到
audit_id,点“通过”就触发interrupt_resume,状态机自动从human_review跳到END;- 若审核拒绝,
update_state()里设review_status="rejected",END节点可捕获并返回“已驳回”。这就是2026年AI Agent的真相:它不是取代人,而是把人的决策点,精准嵌进自动化流程的缝隙里。我带过的7个团队,凡是在
interrupt_before上卡住的,后来都栽在“领导说AI不能自己决定”这句上;而用好它的,都拿到了政务/金融项目的验收签字。
希望帮到你。
本文还有配套的精品资源,点击获取