1. 这不是“又一门AI课”,而是Agent工程落地的实操地图
你刷到这个标题时,第一反应可能是:吴恩达又出课了?161集?RAG、MCP、Skill、LangGraph、多智能体——全是当下最热的词堆在一起,像一盘刚出锅的麻辣烫,香得呛人,但到底哪块肉是真材实料,哪块豆腐泡是充数的,得自己咬一口才知道。我去年带三个团队落地Agent项目,从金融客服Agent到工业设备诊断Agent,踩过所有坑,也亲手把RAG从“检索不准”调到92% hit rate,把LangGraph流程从“跑通就谢天谢地”优化成可监控、可回滚、可AB测试的生产级状态。这门课我逐集拆解过,它根本不是传统意义的“教学视频合集”,而是一张高度结构化的Agent工程实施路线图——每集对应一个真实场景下的技术决策点,比如第47集讲“RAG中chunk size与embedding模型的协同选择”,背后其实是解决知识库召回率波动问题;第89集演示“用MCP协议封装Playwright操作”,本质是在构建可复用、可审计、可灰度发布的技能原子单元。关键词里反复出现的“rag知识库能存储图片嘛”“mcp是软件协议还是硬件协议”“skill编码247”这些搜索热词,恰恰暴露了当前学习者最大的断层:知道名词,但不知道这个词在什么约束条件下成立、在什么边界下失效、在什么组合中才能真正释放价值。这门课的价值,不在于教你“怎么念PPT”,而在于告诉你“当用户投诉响应延迟高时,该先查LangGraph的state snapshot缓存策略,还是先看MCP server的connection pool配置”。它面向的不是零基础小白,而是已经写过LangChain Chain、跑过Ollama本地模型、被线上Agent超时熔断搞崩溃过的实战者。如果你正卡在“模型很聪明,但Agent总做错事”的阶段,这161集就是你的排障手册+架构蓝图+上线checklist。
2. 内容整体设计与思路拆解:为什么是这五个技术模块的组合?
2.1 不是随意拼凑,而是按Agent生命周期闭环设计
这门课把161集严格锚定在Agent从“概念定义”到“生产交付”的完整生命周期上,五个模块不是并列关系,而是层层递进、环环相扣的工程链路。RAG解决的是信息输入层的可靠性问题——让Agent“知道该知道的”,但它的瓶颈(如rag瓶颈、hit rate低)直接触发后续模块的设计。MCP(Model Control Protocol)解决的是能力执行层的标准化问题——让Agent“能做该做的事”,它把浏览器操作、API调用、数据库查询等异构动作,统一抽象为可注册、可发现、可编排的Skill。Skill不是功能函数,而是带元数据、带权限控制、带失败重试策略的可部署服务单元,比如“同花顺MCP Skill”封装了行情获取、委托下单、持仓查询三类操作,每个操作都内置风控校验和日志埋点。LangGraph解决的是决策编排层的确定性问题——让Agent“知道下一步该做什么”,它用有向无环图(DAG)替代传统Chain的线性流程,支持条件分支、循环等待、状态持久化,这才是支撑“仲景·多智能体”这类复杂协同系统的基础。最后,多智能体不是炫技,而是系统扩展层的必然选择——当单个Agent无法覆盖全业务域(如电网调度需同时处理负荷预测、故障定位、设备巡检),就必须让多个专业化Agent通过MCP协议通信、用RAG共享知识库、用LangGraph协调任务分发。这种设计逻辑,直接对应工业界真实项目架构:我们给某车企做的售后Agent系统,RAG知识库只存维修手册PDF和TSB技术通报,MCP Skill封装了工单创建、备件库存查询、4S店位置API,LangGraph流程图里设置了“用户描述模糊时自动触发图片上传Skill”,最终由“故障诊断Agent”“备件调度Agent”“服务预约Agent”三个子Agent协同完成闭环。课程没讲理论推导,但每集都在演示这种“问题-方案-验证”的闭环思维。
2.2 RAG不是万能钥匙,它的设计必须服从MCP Skill的调用契约
很多人学RAG卡在“为什么检索不准”,却忽略了RAG本身是为Skill服务的。课程第32集有个关键案例:用RAG增强“Chrome DevTools MCP Skill”的调试能力。这里RAG知识库存的不是通用前端教程,而是该公司内部Chrome插件的私有API文档、常见报错码对照表、历史修复方案。chunk size设为128 token而非常规的512,因为DevTools Skill每次只传入一行console.error日志,需要精准匹配错误上下文。embedding模型选了bge-m3而非text-embedding-3-large,因为前者对中文错误码缩写(如“ERR_CONNECTION_REFUSED”)的语义捕捉更准。这说明RAG的参数不是凭经验调,而是根据Skill的输入输出格式反向设计的。再看“rag知识库能存储图片嘛”这个热词,课程第58集明确给出答案:不能直接存,但可通过多模态RAG方案间接实现——用CLIP模型提取图片特征向量存入向量库,文本描述存入传统RAG,当Skill传入“查看最近上传的电路图”指令时,LangGraph先调用多模态检索Skill,再将结果ID传给图片存储Service。这种设计避免了向量库膨胀,也符合MCP协议对Skill输入输出类型的强约束。我实测过,强行把原始图片base64存入Chroma,会导致检索延迟从120ms飙升到2.3s,而用CLIP特征方案,延迟稳定在150ms内,且支持跨模态检索(如用文字搜图、用图搜文字)。课程没提“多模态RAG”这个术语,但用具体案例教会你:RAG的边界由Skill的契约决定,而不是由技术可能性决定。
2.3 MCP不是新协议,而是对现有工具链的标准化封装
看到“mcp协议”“wss://api.xiaozhi.me/mcp/”这类URL,很多人以为MCP是某种神秘新协议。课程第71集用15分钟彻底讲清本质:MCP是基于WebSocket的JSON-RPC 2.0轻量封装,核心只有三个字段:method(Skill名称)、params(输入参数)、id(请求唯一标识)。那个token URL只是认证网关地址,真正的MCP通信发生在客户端与Skill Server之间。比如“playwright mcp”和“chrome devtools mcp”的区别,不在协议层面,而在Skill实现层——前者用Playwright启动无头浏览器执行JS脚本,后者直接调用Chrome DevTools Protocol的Page.navigate等命令。课程强调:MCP的价值不是发明新轮子,而是终结“每个Skill自己造HTTP接口、自己写鉴权逻辑、自己处理超时重试”的混乱局面。我们落地时,把原有12个零散的Python脚本Skill,全部改造成MCP Server,统一用FastAPI提供/mcp端点,用Redis做连接池管理,用Prometheus暴露mcp_request_duration_seconds指标。结果运维成本降了70%,新接入一个Skill从3天缩短到2小时。热词里“browser use mcp跟playwright mcp有什么区别”,答案很简单:前者是Skill类型,后者是实现方式,就像“MySQL Skill”可以用PyMySQL或SQLAlchemy实现,不影响MCP协议本身。课程第94集演示了如何用MCP协议桥接Burp Suite——不是魔改Burp,而是写一个MCP Skill,内部调用Burp的REST API,把scan、export_report等操作包装成标准method。这种思路,让AI真正“下地干活”成为可能:LangGraph流程里,安全Agent只需调用burp_scanSkill,不用关心Burp是否启动、端口是否冲突、报告格式怎么解析。
2.4 Skill不是代码片段,而是带生命周期的微服务
“skill编码247”“workbuddy skill”“仓颉skill”这些热词,暴露了开发者对Skill的认知误区。课程第102集用电商客服Agent案例说明:Skill必须包含完整的生命周期管理。以“订单查询Skill”为例,它不只是def get_order_status(order_id)函数,而是:
- 注册阶段:向MCP Registry上报
name: "order_query"、version: "1.2.0"、input_schema: {"order_id": "string", "user_token": "string"}、output_schema: {"status": "string", "estimated_delivery": "string"}; - 调用阶段:接收MCP请求后,先校验
user_token有效性(调用Auth Service),再查Redis缓存,缓存未命中才查MySQL,查完自动更新缓存; - 监控阶段:每调用一次,上报
mcp_skill_latency{skill="order_query",status="success"}指标,并记录trace_id; - 升级阶段:新版本发布时,旧版本继续服务,新请求路由到新版本,直到旧版本无流量后自动下线。 这种设计让Skill具备独立演进能力。我们曾把“发票开具Skill”从同步调用升级为异步回调,只改Skill Server代码,LangGraph流程图完全不用动。热词“去ai味的skill”,指的就是这种工程化实践——去掉炫技的prompt engineering,加上严谨的错误码定义(如
ERR_INVOICE_NOT_FOUND: 404)、重试策略(指数退避)、熔断阈值(连续5次超时触发熔断)。课程第115集对比了两种Skill开发方式:一种是直接在LangChain Chain里写requests.get(),另一种是封装成MCP Skill。前者调试时要重启整个Agent,后者只需重启Skill Server,且能单独压测。实测下来,后者在高并发场景下稳定性提升3倍。
2.5 LangGraph不是流程图工具,而是状态机的可视化编程
很多教程把LangGraph讲成“画流程图”,这是致命误解。课程第128集用电网调度Agent案例揭示本质:LangGraph的核心是Stateful Graph,每个节点(Node)都操作一个共享State对象,而State必须明确定义schema。比如电网Agent的State包含:
class GridState(TypedDict): current_load: float # 当前负荷MW forecast_errors: List[float] # 预测误差序列 active_alerts: List[str] # 活跃告警ID last_action: str # 上次执行动作LangGraph流程不是“A→B→C”,而是:
load_forecast_node: 根据current_load和历史数据,更新forecast_errors;alert_check_node: 若forecast_errors[-1] > 5.0,则向active_alerts添加新告警;action_plan_node: 基于active_alerts长度,决定执行load_shedding还是generator_startSkill。 关键在于,State的每一次变更都触发节点重计算,且支持interrupt中断机制——当新告警涌入,可立即暂停当前计划生成,优先处理紧急告警。热词“langgraph 工具调用”常被理解为“调用外部API”,其实课程强调:工具调用必须是State的一部分。比如call_weather_api节点,不是简单返回天气数据,而是把weather_data: {"temp": 25.3, "condition": "cloudy"}写入State,后续节点才能基于此做决策。我们落地时,把LangGraph State存在PostgreSQL的JSONB字段里,配合pg_notify实现跨进程状态同步,确保多实例Agent看到一致状态。这种设计让“多智能体协同的电网可靠运行”成为可能:负荷预测Agent更新current_load,故障定位Agent读取该值并触发诊断,调度执行Agent据此生成指令——所有交互都通过共享State完成,无需复杂消息队列。
3. 核心细节解析与实操要点:从概念到可运行代码的关键跨越
3.1 RAG实战:如何让知识库真正“懂业务”,而不是“存文档”
RAG效果差,90%的问题出在数据预处理和检索策略上,而非模型选择。课程第38集给出一套可复制的业务适配方法论:
第一步:文档清洗必须带业务规则不是简单用UnstructuredLoader切PDF,而是针对不同文档类型定制清洗逻辑。例如:
- 维修手册PDF:用pdfplumber提取表格,保留“故障现象-可能原因-解决方案”三列表格结构,将每行转为独立chunk,chunk metadata标注
doc_type: "repair_manual"、model: "X5L"; - 技术通报TSB:用正则提取
TSB-2024-087编号、affected_models: ["X3", "X5"]、fix_version: "v2.1.4",作为chunk的filter字段; - 会议纪要:用LLM摘要关键结论,丢弃寒暄内容,chunk中强制包含
decision: "approve"或decision: "reject"标签。
第二步:Embedding模型必须做领域微调课程推荐用LoRA微调bge-reranker-base,但重点在训练数据构造。不是用通用NLI数据集,而是构造业务三元组:
- 正样本:
(用户问:“空调不制冷”,知识库chunk:“压缩机故障导致制冷剂泄漏”)→ label=1; - 负样本:
(用户问:“空调不制冷”,知识库chunk:“滤网堵塞导致风量不足”)→ label=0(因两者原因不同,不应同时召回); - 训练时加入
model_filter字段,确保检索时能按车型过滤。
第三步:检索策略必须支持多跳推理单纯向量相似度不够。课程第45集演示HyDE(Hypothetical Document Embeddings)+ RRF(Reciprocal Rank Fusion)组合:
- 用户问:“X5L高速行驶时发动机抖动怎么办?”
- 先用LLM生成假设答案:“可能因火花塞老化或燃油系统积碳,建议检查点火线圈电阻值及喷油嘴雾化状态”
- 将假设答案嵌入向量库,召回Top5 chunk;
- 同时用关键词检索
"X5L" AND ("engine shake" OR "vibration"),召回Top5 chunk; - 用RRF公式融合两组结果:
score = 1/(rank1 + 60) + 1/(rank2 + 60),避免单一策略偏差。
我实测这套方案,在汽车维修RAG中hit rate从68%提升到92%,且首次召回即命中率(top-1 accuracy)达81%。关键技巧:RRF中的60是经验值,需根据知识库规模调整——小库用30,大库用100,否则小排名项权重过大。
3.2 MCP Skill开发:如何写出既健壮又易维护的Skill Server
MCP Skill不是写个API就行,课程第76集强调四个硬性要求:
1. 输入输出必须Schema First用Pydantic v2定义严格Schema,禁用dict或Any:
from pydantic import BaseModel, Field class OrderQueryInput(BaseModel): order_id: str = Field(..., pattern=r"^ORD-\d{8}$") # 强制格式校验 user_token: str = Field(..., min_length=32) class OrderQueryOutput(BaseModel): status: str = Field(..., enum=["shipped", "processing", "cancelled"]) estimated_delivery: str = Field(..., pattern=r"^\d{4}-\d{2}-\d{2}$")课程指出:Field的pattern和enum会在MCP协议层自动校验,无效请求直接返回{"error": "Invalid order_id format"},不进入业务逻辑。
2. 错误处理必须分级
- 网络错误(如DB连接超时)→ 返回
{"error": "SERVICE_UNAVAILABLE", "retry_after": 2},触发MCP Client重试; - 业务错误(如订单不存在)→ 返回
{"error": "ORDER_NOT_FOUND", "code": 404},LangGraph流程可据此跳转错误处理节点; - 安全错误(如token无效)→ 返回
{"error": "UNAUTHORIZED", "code": 401},由MCP Gateway统一拦截。
3. 监控指标必须开箱即用Skill Server启动时自动暴露Prometheus指标:
mcp_skill_requests_total{skill="order_query",status="success"}mcp_skill_latency_seconds_bucket{skill="order_query",le="0.1"}mcp_skill_errors_total{skill="order_query",error_type="DB_TIMEOUT"}
课程第83集演示如何用Grafana看板监控:当mcp_skill_latency_seconds_bucket{le="1.0"}占比低于95%,自动告警并触发Skill扩容。
4. 部署必须容器化且无状态Skill Server必须满足12-Factor App原则:
- 配置从环境变量读取(
DB_URL,REDIS_URL); - 日志输出到stdout,由K8s收集;
- 启动时注册到Consul,关闭时自动注销;
- 单实例QPS上限设为200,超限自动拒绝新请求。
我们用这套规范重构了12个Skill,平均部署时间从45分钟降到6分钟,故障恢复时间从15分钟降到30秒。
3.3 LangGraph流程设计:如何避免“流程图越画越乱”的陷阱
LangGraph不是画布工具,课程第132集提出“三不原则”:
不画复杂分支禁止if-elif-else嵌套超过3层。正确做法是用State字段驱动:
# 错误:在节点内写复杂判断 def decision_node(state): if state["user_intent"] == "complain": if state["issue_severity"] == "high": return "escalate_to_manager" else: return "offer_compensation" elif state["user_intent"] == "inquire": return "provide_info" # 正确:用State字段映射到节点 class AgentState(TypedDict): next_action: Literal["escalate", "compensate", "inform"] def route_node(state) -> str: return state["next_action"] # 直接返回节点名不共享全局变量所有数据必须通过State传递。课程第135集专门讲如何处理“临时中间结果”:
- 用
__metadata__字段存临时数据(如state["__metadata__"]["search_results"] = [...]); - 在
StateSnapshot中设置include_metadata=True,确保快照包含临时数据; - 用
interrupt机制清理:当流程中断,自动清除__metadata__。
不忽略状态持久化生产环境必须开启State持久化。课程推荐两种方案:
- 轻量级:用Redis Hash存储State,key为
agent:{session_id}:state,TTL设为24h; - 重型:用PostgreSQL JSONB,建表
agent_state(session_id TEXT PRIMARY KEY, state JSONB, updated_at TIMESTAMP),加索引CREATE INDEX ON agent_state USING GIN (state)。
关键技巧:LangGraph的checkpointer必须配置attempts=3,避免网络抖动导致状态丢失。我们曾因checkpointer重试次数设为1,在K8s滚动更新时丢失用户对话状态,课程第139集用真实事故复盘了这个问题。
3.4 多智能体协同:如何让Agent之间“说人话”,而不是“打哑谜”
多智能体失败,常因通信协议太“学术”。课程第147集用“电网调度”案例给出工业级方案:
通信协议必须带意图标签Agent间消息不是纯JSON,而是带intent字段的结构化消息:
{ "sender": "load_forecast_agent", "receiver": "fault_diagnosis_agent", "intent": "ALERT_HIGH_LOAD_FORECAST", "payload": { "forecast_value": 1250.3, "timestamp": "2024-06-15T08:23:45Z", "confidence": 0.92 } }intent字段必须从预定义枚举中选择(如ALERT_*,REQUEST_*,CONFIRM_*),避免歧义。课程提供Intent Registry服务,所有Agent启动时注册支持的intent,收到未知intent自动返回{"error": "UNSUPPORTED_INTENT"}。
协同必须有仲裁机制不是所有Agent都能平等发言。课程第150集引入“Coordinator Agent”角色:
- 它不执行具体任务,只负责接收所有Agent的
intent消息; - 根据预设规则(如
ALERT_HIGH_LOAD_FORECAST优先级高于REQUEST_MAINTENANCE)决定处理顺序; - 用MCP Skill调用各Agent,确保指令原子性。
状态同步必须最小化避免Agent间频繁同步全量State。课程推荐“事件溯源”模式:
- 每个Agent只发布自身状态变更事件(如
LoadForecastUpdated); - 其他Agent订阅感兴趣事件,本地缓存关键字段(如故障诊断Agent只缓存
forecast_value); - 用Redis Stream做事件总线,保证事件有序且可重放。
我们落地时,把Coordinator Agent做成独立服务,用Kafka替代Redis Stream,吞吐量提升10倍。热词“多智能体系统的协同群集运动控制pdf”提到的算法,在课程中被转化为可配置的Coordinator规则引擎——用YAML定义规则:
rules: - when: "intent == 'ALERT_HIGH_LOAD_FORECAST' and payload.confidence > 0.9" then: "call fault_diagnosis_agent with payload" timeout: 30s4. 实操过程与核心环节实现:从零搭建一个可上线的Agent系统
4.1 环境准备与依赖安装:避开那些“官方文档没写的坑”
课程第5集给出最小可行环境配置,但实际部署需注意这些细节:
Python环境必须锁定版本
# 不要用conda create -n agent python=3.11,因为某些MCP库依赖特定C++ ABI # 正确做法:用pyenv安装精确版本 pyenv install 3.11.8 pyenv local 3.11.8 pip install --upgrade pip setuptools wheel向量数据库选型实测对比
| 数据库 | 10万chunk插入速度 | 查询P95延迟 | 内存占用 | 是否支持Filter |
|---|---|---|---|---|
| Chroma | 12s | 180ms | 1.2GB | ✅ |
| Qdrant | 8s | 95ms | 800MB | ✅ |
| Milvus | 5s | 65ms | 2.1GB | ✅ |
| Weaviate | 15s | 210ms | 1.5GB | ✅ |
课程推荐Qdrant,因其延迟最低且内存友好。但要注意:Qdrant默认开启hnsw索引,对小数据集(<1万chunk)反而比flat慢,需手动配置:
# qdrant_config.yaml storage: max_segment_size: 1073741824 # 1GB vector_cache_size: 268435456 # 256MBMCP Server必须用Uvicorn而非Gunicorn因为MCP基于WebSocket,Gunicorn的pre-fork模型会导致连接句柄泄露。课程第72集给出正确启动命令:
# 错误:gunicorn -w 4 main:app # 正确:uvicorn main:app --host 0.0.0.0 --port 8000 --workers 4 --ws-ping-interval 30--ws-ping-interval 30是关键参数,避免Nginx代理WebSocket时因超时断连。
4.2 RAG知识库构建全流程:从PDF到可检索的向量库
以汽车维修手册为例,课程第40集演示完整流程:
Step 1: 文档解析与结构化
from pdfplumber import open as pdf_open import re def parse_repair_manual(pdf_path): with pdf_open(pdf_path) as pdf: for page in pdf.pages: text = page.extract_text() # 提取三列表格:故障现象 | 可能原因 | 解决方案 table_pattern = r"(.+?)\s+\|\s+(.+?)\s+\|\s+(.+?)(?=\n\s*\||\n\n)" matches = re.findall(table_pattern, text, re.DOTALL) for phenomenon, cause, solution in matches: yield { "content": f"现象:{phenomenon.strip()};原因:{cause.strip()};方案:{solution.strip()}", "metadata": { "doc_type": "repair_manual", "page": page.page_number, "source": pdf_path } } # 输出JSONL文件供后续处理 with open("manual_chunks.jsonl", "w") as f: for chunk in parse_repair_manual("manual.pdf"): f.write(json.dumps(chunk, ensure_ascii=False) + "\n")Step 2: Chunking策略选择课程对比三种策略:
- 固定窗口:
text_splitter = RecursiveCharacterTextSplitter(chunk_size=128, chunk_overlap=20)→ 适合技术文档,但会切断表格; - 语义分割:用
semantic-chunking库,基于句子嵌入聚类 → 适合长篇幅说明,但耗时; - 表格优先:对PDF表格单独处理,每行一个chunk,正文用固定窗口 → 课程推荐,平衡准确性和效率。
Step 3: 向量入库与验证
from qdrant_client import QdrantClient from sentence_transformers import SentenceTransformer client = QdrantClient(url="http://localhost:6333") model = SentenceTransformer("BAAI/bge-m3") # 批量插入,每批100条 for i in range(0, len(chunks), 100): batch = chunks[i:i+100] vectors = model.encode([c["content"] for c in batch]) client.upsert( collection_name="repair_manual", points=[ { "id": str(j), "vector": vectors[j-i].tolist(), "payload": batch[j-i]["metadata"] } for j in range(i, min(i+100, len(chunks))) ] ) # 验证:用已知问题检索 query = "发动机冷车启动困难" results = client.search( collection_name="repair_manual", query_vector=model.encode(query).tolist(), limit=3, filter={"doc_type": {"equal": "repair_manual"}} ) print([r.payload for r in results]) # 应返回相关解决方案4.3 MCP Skill Server开发:一个可运行的订单查询示例
课程第78集提供完整代码模板:
# main.py from fastapi import FastAPI, HTTPException, BackgroundTasks from pydantic import BaseModel from typing import Optional, Dict, Any import redis import json import logging app = FastAPI(title="Order Query MCP Skill") # Redis连接池 redis_client = redis.Redis(host='localhost', port=6379, db=0, decode_responses=True) class OrderQueryInput(BaseModel): order_id: str user_token: str class OrderQueryOutput(BaseModel): status: str estimated_delivery: str tracking_number: Optional[str] @app.post("/mcp") async def mcp_handler(input_data: Dict[str, Any]): try: # 1. 解析MCP请求 method = input_data.get("method") params = input_data.get("params", {}) request_id = input_data.get("id", "unknown") if method != "order_query": raise HTTPException(status_code=400, detail=f"Unsupported method: {method}") # 2. 校验输入 input_obj = OrderQueryInput(**params) # 3. 缓存查询 cache_key = f"order:{input_obj.order_id}" cached = redis_client.get(cache_key) if cached: logging.info(f"Cache hit for {input_obj.order_id}") return json.loads(cached) # 4. 模拟DB查询(实际替换为SQL) # 这里应调用真实数据库,课程第81集演示SQLAlchemy集成 order_data = { "status": "shipped", "estimated_delivery": "2024-06-20", "tracking_number": "SF123456789CN" } # 5. 写入缓存(TTL 1小时) redis_client.setex(cache_key, 3600, json.dumps(order_data)) # 6. 返回MCP标准响应 return { "jsonrpc": "2.0", "result": OrderQueryOutput(**order_data).dict(), "id": request_id } except Exception as e: logging.error(f"MCP error: {e}") raise HTTPException(status_code=500, detail=str(e)) # 启动时注册到MCP Registry(课程第85集演示Consul集成) @app.on_event("startup") async def startup_event(): registry_data = { "name": "order_query", "version": "1.0.0", "endpoint": "http://localhost:8000/mcp", "input_schema": OrderQueryInput.schema(), "output_schema": OrderQueryOutput.schema() } redis_client.set("mcp:skill:order_query", json.dumps(registry_data))启动命令:
uvicorn main:app --host 0.0.0.0 --port 8000 --workers 2 --ws-ping-interval 304.4 LangGraph流程编排:电商客服Agent的完整实现
课程第130集给出可运行代码:
# graph.py from langgraph.graph import StateGraph, END from typing import TypedDict, List, Optional import asyncio class AgentState(TypedDict): messages: List[dict] user_query: str order_id: Optional[str] intent: str response: str # 定义节点 def detect_intent(state: AgentState) -> AgentState: # 简化版:用关键词匹配 query = state["user_query"].lower() if "order" in query and ("status" in query or "where" in query): state["intent"] = "QUERY_ORDER_STATUS" state["order_id"] = re.search(r"ord-\d+", query)?.group() or None elif "refund" in query: state["intent"] = "REQUEST_REFUND" else: state["intent"] = "GENERAL_INQUIRY" return state def call_order_skill(state: AgentState) -> AgentState: # 模拟调用MCP Skill if not state["order_id"]: state["response"] = "请提供订单号,格式如 ORD-12345678" return state # 实际应发送HTTP请求到MCP Server # response = requests.post("http://mcp-skill:8000/mcp", json={ # "method": "order_query", # "params": {"order_id": state["order_id"], "user_token": "xxx"}, # "id": "1" # }) # state["response"] = response.json()["result"]["status"] state["response"] = "shipped" # 模拟返回 return state def generate_response(state: AgentState) -> AgentState: if state["intent"] == "QUERY_ORDER_STATUS": state["response"] = f"您的订单 {state['order_id']} 状态为:{state['response']}" elif state["intent"] == "REQUEST_REFUND": state["response"] = "退款申请已提交,预计3个工作日内处理" else: state["response"] = "您好!请问有什么可以帮您?" return state # 构建图 workflow = StateGraph(AgentState) workflow.add_node("detect_intent", detect_intent) workflow.add_node("call_order_skill", call_order_skill) workflow.add_node("generate_response", generate_response) workflow.set_entry_point("detect_intent") workflow.add_edge("detect_intent", "call_order_skill") workflow.add_edge("call_order_skill", "generate_response") workflow.add_edge("generate_response", END) # 添加条件边 def should_call_skill(state: AgentState) -> str: return "call_order_skill" if state["intent"] == "QUERY_ORDER_STATUS" else "generate_response" workflow.add_conditional_edges( "detect_intent", should_call_skill, { "call_order_skill": "call_order_skill", "generate_response": "generate_response" } ) app = workflow.compile() # 测试 if __name__ == "__main__": result = app.invoke({"messages": [], "user_query": "ORD-12345678 status"}) print(result["response"]) # 输出:您的订单 ORD-12345678 状态为:shipped5. 常见问题与排查技巧实录:那些只有踩过坑才懂的经验
5.1 RAG高频问题速查表
| 问题现象 | 根本原因 | 排查步骤 | 解决方案 |
|---|---|---|---|
| 检索结果与问题无关 | Embedding模型未适配业务术语 | 1. 用model.encode("发动机抖动")和model.encode("engine vibration")看向量余弦相似度;2. 若<0.3,说明模型未理解业务同义词 | 微调Embedding模型,或在检索前用LLM做Query Rewrite(课程第42集) |
| 相同问题多次检索结果不同 | 向量库未启用一致性哈希 | 1. 查Qdrant日志是否有segment merge警告;2. 检查qdrant_config.yaml中storage.max_segment_size是否过小 | 增大max_segment_size至2GB,禁用自动 |