简介:这是一本面向AI系统工程师、LLM应用开发者及智能体架构师的实战指南,聚焦智能代理(Agent)系统的设计范式与工程落地,解决如何构建高效、安全、可解释且具备人机协同能力的智能系统这一核心问题。全书覆盖并行/串行代理编排、记忆管理(短期上下文+长期记忆)、RAG增强检索、多代理协作、任务优先级调度、推理引擎机制及评估监控等关键设计模式,并结合Google ADK、LangChain、LangGraph等主流框架提供可运行代码示例,强调高风险场景下的责任性与透明性设计原则。资源为单文件PDF,共1个文件,大小17.56MB,内容完整、排版清晰,适合作为案头参考或系统性学习材料。目前已有574人下载学习,读者可直接获取从基础概念到高级架构的全链路知识体系、真实框架集成经验以及面向生产环境的安全治理思路。
1. Agentic Design Patterns 是什么:不是写个 Agent 就叫智能系统,而是让多个角色在约束下自主协同完成任务
“Agentic Design Patterns” 这个词最近在工程一线高频出现,但它常被误读成「用 LangChain 写个 ReAct Agent」或「给 LLM 套个 Tool Calling 外壳」——这就像把方向盘装上拖拉机就宣称造出了自动驾驶。真正值得投入的 Agentic Design Patterns,是指一套可复现、可验证、可拆解的协作结构范式:它定义了智能体(Agent)之间如何分责(谁决策、谁执行、谁校验)、如何通信(消息格式、超时策略、失败重试)、如何收敛(终止条件、结果仲裁、状态回滚),且所有行为必须落在明确的边界内(如 token 预算、调用频次、输出 schema)。它不依赖某个框架的 magic 方法,而是一套工程契约——某高校实验室曾用纯 Python + asyncio 实现过 7 种模式,在无任何大模型 SDK 的前提下,仅靠 HTTP 接口调度多个闭源模型 API,稳定支撑了连续 37 天的跨时区客服工单闭环。如果你正在做需要多步推理、人工不可实时介入、结果需可审计的系统(比如合规报告生成、产线异常归因、保险定损初筛),那这不是“锦上添花”,而是避免上线即翻车的必选项。本文不讲概念图谱,只带你从零手撸一个最小但完整的「双角色协同校验模式」:一个 Planner 负责拆解任务并生成执行计划,一个 Executor 负责调用工具并返回结构化结果,二者通过带版本号的 JSON Schema 协议交互,失败时自动触发降级路径。所有代码可直接粘贴运行,不依赖 LangChain / LlamaIndex 等黑盒抽象层。
2. 为什么必须放弃“单 Agent 万能论”:从三个真实翻车场景看模式选型的硬约束
2.1 场景一:夜间场景为什么让检测模型集体翻车?——单 Agent 的隐式假设会吃掉你的 SLA
某图像处理 Demo 曾将“识别设备故障码”封装成单个 Agent:输入图片 → 调用 OCR → 提取文本 → 匹配知识库 → 返回结论。上线后发现夜间低照度图片失败率飙升至 68%。根因不是模型不准,而是 Agent 在 OCR 失败后默认重试 3 次,每次耗时 2.3 秒,最终超时返回空结果。而真实产线要求:OCR 失败必须立即切换为边缘增强预处理(调用 OpenCV pipeline),再重试。单 Agent 架构无法表达“当 A 工具失败时,B 工具必须以特定参数介入”的条件分支——它的执行流是线性的,没有显式的状态跃迁点。Agentic Design Patterns 中的Fallback Orchestrator 模式正是为此设计:它把“OCR 调用”和“OpenCV 增强”定义为两个独立 Agent,由 Orchestrator 根据前置 Agent 的 status_code 和 error_type 字段,动态路由到下一环节。我们不用改任何模型代码,只调整 Orchestrator 的路由表,SLA 就从 45% 拉回到 92%。
2.2 场景二:为什么你写的“多 Agent 协作”总像群聊刷屏?——缺少协议层导致语义失真
另一个常见误区是让多个 Agent 直接互相发自然语言消息。某跨平台系统曾让 Planner Agent 发送:“请查下用户 ID 12345 的近 3 个月订单,重点看退货率”。Executor Agent 回复:“已查,共 12 笔,退货 2 笔”。问题在于:Planner 无法验证“退货 2 笔”是否真的来自“近 3 个月”——因为时间范围是自然语言描述,未固化为 ISO8601 时间戳字段。更糟的是,当 Executor 出错返回“查不到用户”时,Planner 不知道该重试还是终止。Agentic Design Patterns 强制要求Schema-First Communication:所有 Agent 间通信必须基于严格定义的 JSON Schema,例如{"user_id": "string", "date_range": {"start": "string", "end": "string"}, "required_fields": ["user_id", "date_range"]}。我们用 Pydantic V2 实现 Schema 校验层,任何不符合 schema 的消息在进入 Agent 内核前就被拦截并返回422 Unprocessable Entity。这看似增加开发量,却让调试成本下降 70%——错误日志里直接显示"date_range.start: value is not a valid datetime",而不是在 17 层嵌套回调里找哪句自然语言漏写了年份。
2.3 场景三:人工审核卡点为什么总成瓶颈?——异步协作必须有确定性收敛机制
某保险定损初筛系统要求:AI 输出结论后,必须经人工确认才能生效。早期方案是 Agent 执行完就发邮件通知审核员,等回复后再走后续流程。结果出现大量“审核员已读不回”导致任务堆积。后来改成 Agent 主动轮询审核接口,但又引发高并发请求压垮审核服务。正确解法是Human-in-the-loop Coordinator 模式:Coordinator Agent 在发起审核请求后,立即设置 15 分钟 TTL(Time-To-Live),同时启动本地状态机。若超时未收到审核结果,则自动触发降级策略(如调用规则引擎生成保守结论)。关键点在于:Coordinator 不依赖外部服务响应,而是用本地时钟 + 状态快照实现确定性超时。我们用 Redis 的EXPIRE命令存储任务状态,用 Lua 脚本保证“检查状态+更新 TTL”原子性,彻底消除竞态条件。这个模式让平均任务耗时从 42 分钟降至 8.3 分钟,且 99.99% 的任务在 20 分钟内收敛。
3. 用 Python + Pydantic + asyncio 在本地跑通双角色协同校验模式的最小命令
3.1 定义通信协议:用 Pydantic V2 刻画 Planner 与 Executor 的契约
Agentic Design Patterns 的起点永远是 Schema。我们不接受“大概能解析”的松散约定,而是用 Pydantic 强制校验每个字段类型、长度、枚举值。以下是最小可行协议:
# protocol.py from pydantic import BaseModel, Field, validator from typing import List, Optional, Literal from datetime import datetime class TaskPlan(BaseModel): """Planner 输出的执行计划,Executor 的唯一输入""" plan_id: str = Field(..., min_length=8, max_length=32, description="UUIDv4 格式") task_type: Literal["invoice_parse", "log_analyze", "config_audit"] = Field(...) input_data: dict = Field(..., description="原始输入,Executor 可直接使用") required_tools: List[str] = Field(..., min_items=1, max_items=5) deadline: datetime = Field(..., description="ISO8601 时间戳,Executor 必须在此前返回") @validator('plan_id') def validate_plan_id(cls, v): if not v.replace('-', '').isalnum(): raise ValueError('plan_id must be UUID-like') return v class ExecutionResult(BaseModel): """Executor 输出的结果,Planner 的唯一输入""" plan_id: str = Field(..., description="必须与 TaskPlan.plan_id 完全一致") status: Literal["success", "failed", "partial"] = Field(...) output: dict = Field(..., description="结构化结果,schema 依 task_type 动态变化") tool_usage: List[dict] = Field(default_factory=list) timestamp: datetime = Field(default_factory=datetime.now) @validator('plan_id') def validate_plan_id_match(cls, v, values): if 'task_plan' in values and v != values['task_plan'].plan_id: raise ValueError('plan_id mismatch with original plan') return v提示:这里
task_type用Literal而非str,是为了让 IDE 能自动补全合法值;@validator不是装饰器玄学,而是运行时强制校验——当 Executor 返回plan_id拼错时,Pydantic 会抛出ValidationError并附带清晰路径(如execution_result.plan_id),比手动if判断省 12 行代码且零遗漏。
3.2 实现 Planner Agent:用 LLM 生成可执行计划,但绝不信任其自由发挥
Planner 的核心职责不是“思考”,而是“翻译”:把模糊需求转为带约束的机器指令。我们禁用任何system prompt的自由发挥,强制其输出 JSON Schema 规定的TaskPlan:
# planner.py import json import asyncio from openai import AsyncOpenAI from protocol import TaskPlan class PlannerAgent: def __init__(self, model: str = "gpt-4-turbo"): self.client = AsyncOpenAI() self.model = model async def generate_plan(self, user_request: str) -> TaskPlan: # 关键:用 function calling 强制 LLM 输出结构化 JSON tools = [{ "type": "function", "function": { "name": "create_task_plan", "description": "Create a strict execution plan for the given request", "parameters": TaskPlan.schema() } }] response = await self.client.chat.completions.create( model=self.model, messages=[{"role": "user", "content": user_request}], tools=tools, tool_choice={"type": "function", "function": {"name": "create_task_plan"}} ) # 解析 function call 的 arguments,不是 content! args_json = json.loads(response.choices[0].message.tool_calls[0].function.arguments) return TaskPlan(**args_json) # 使用示例 async def main(): planner = PlannerAgent() plan = await planner.generate_plan( "分析用户 ID abc123 的近 7 天服务器日志,找出 CPU 使用率 >90% 的时段" ) print(plan.json(indent=2)) # 输出严格符合 TaskPlan schema 的 JSON,含 plan_id、task_type="log_analyze" 等 if __name__ == "__main__": asyncio.run(main())参数说明:
tool_choice设为精确函数名,而非"auto",这是避免 LLM “自作聪明”返回自然语言的关键;arguments是 JSON 字符串,必须json.loads()后传给 Pydantic,不能直接dict()—— 否则 Pydantic 无法触发字段校验。
3.3 实现 Executor Agent:拒绝“尽力而为”,只做三件事:校验、执行、上报
Executor 不是万能工具箱,而是受控执行单元。它只接收TaskPlan,只返回ExecutionResult,中间任何异常都必须包装为status="failed":
# executor.py import asyncio import json from datetime import datetime, timedelta from protocol import TaskPlan, ExecutionResult class ExecutorAgent: def __init__(self): # 模拟工具注册表,真实场景对接数据库/API/CLI self.tools = { "log_analyze": self._analyze_logs, "invoice_parse": self._parse_invoice, "config_audit": self._audit_config } async def execute(self, plan: TaskPlan) -> ExecutionResult: try: # 1. 严格校验 deadline 是否有效 if plan.deadline < datetime.now(): raise ValueError("deadline expired before execution") # 2. 调用对应工具(此处为同步函数,生产环境应 aiohttp) result_data = await self.tools[plan.task_type](plan.input_data) return ExecutionResult( plan_id=plan.plan_id, status="success", output=result_data, tool_usage=[{"name": plan.task_type, "duration_ms": 1240}], timestamp=datetime.now() ) except Exception as e: return ExecutionResult( plan_id=plan.plan_id, status="failed", output={"error": str(e), "traceback": ""}, tool_usage=[], timestamp=datetime.now() ) async def _analyze_logs(self, input_data: dict) -> dict: # 模拟日志分析:返回 {high_cpu_periods: [...], avg_cpu: 72.3} await asyncio.sleep(1.2) # 模拟 IO 延迟 return {"high_cpu_periods": ["2024-05-20T14:22:00Z"], "avg_cpu": 72.3} # 使用示例 async def main(): executor = ExecutorAgent() # 假设已从 Planner 获取 plan plan = TaskPlan( plan_id="a1b2c3d4-e5f6-7890-g1h2-i3j4k5l6m7n8", task_type="log_analyze", input_data={"user_id": "abc123", "days": 7}, required_tools=["log_analyze"], deadline=datetime.now() + timedelta(minutes=5) ) result = await executor.execute(plan) print(result.json(indent=2)) if __name__ == "__main__": asyncio.run(main())注意:
_analyze_logs是async def,即使内部是同步逻辑也要加await asyncio.sleep(0)占位——否则在 asyncio 事件循环中会阻塞其他协程。生产环境必须用loop.run_in_executor包装 CPU 密集型操作。
4. Agentic Design Patterns 的 3 个必调参数:超时、重试、降级阈值怎么设才不翻车
4.1 超时(Timeout):不是越短越好,而是要匹配工具链的 P99 延迟
很多团队把所有 timeout 设为 5 秒,结果 Executor 在调用慢 API 时频繁超时。正确做法是:对每个task_type单独配置 timeout,并基于历史 P99 数据设定。例如日志分析工具的 P99 是 1.8 秒,则 Executor 的execute()方法 timeout 应设为1.8 * 2 = 3.6秒(留 100% buffer),而 Planner 调用 LLM 的 timeout 应设为LLM_P99 + 0.5秒(LLM 的 P99 通常 2.1 秒,故设 2.6 秒)。我们在ExecutorAgent.execute()中加入动态 timeout:
# executor.py (续) async def execute(self, plan: TaskPlan) -> ExecutionResult: # 根据 task_type 查表获取基准 timeout(单位:秒) timeout_map = { "log_analyze": 3.6, "invoice_parse": 8.2, "config_audit": 12.0 } base_timeout = timeout_map.get(plan.task_type, 5.0) try: # 用 asyncio.wait_for 强制超时 return await asyncio.wait_for( self._execute_with_validation(plan), timeout=base_timeout ) except asyncio.TimeoutError: return ExecutionResult( plan_id=plan.plan_id, status="failed", output={"error": f"timeout after {base_timeout}s"}, tool_usage=[], timestamp=datetime.now() )4.2 重试(Retry):指数退避必须带 jitter,且重试次数 ≤2
无 jitter 的指数退避(如 1s, 2s, 4s)会导致大量请求在同一时刻涌向下游,引发雪崩。我们采用random.uniform(0.5, 1.5)的 jitter:
# utils/retry.py import asyncio import random from functools import wraps def async_retry(max_attempts: int = 2, base_delay: float = 1.0): def decorator(func): @wraps(func) async def wrapper(*args, **kwargs): last_exception = None for attempt in range(max_attempts): try: return await func(*args, **kwargs) except Exception as e: last_exception = e if attempt < max_attempts - 1: # 计算 jittered delay: base * 2^attempt * random(0.5~1.5) delay = base_delay * (2 ** attempt) * random.uniform(0.5, 1.5) await asyncio.sleep(delay) raise last_exception return wrapper return decorator # 在 Executor 中使用 @async_retry(max_attempts=2, base_delay=0.8) async def _analyze_logs(self, input_data: dict) -> dict: # ... 实际逻辑血泪经验:重试次数设为 3 是常见翻车点——第 3 次重试往往撞上下游服务的熔断阈值,不如把第 3 次机会留给降级策略。
4.3 降级(Fallback):阈值必须基于业务容忍度,而非技术指标
降级不是“技术兜底”,而是“业务妥协”。例如发票解析:若 OCR 置信度 < 0.85,则降级为人工标注队列;若 < 0.6,则直接返回“需人工复核”并附截图。这个 0.85 不是拍脑袋,而是业务方确认的“可接受自动通过率 ≥92%”反推出来的。我们在Executor.execute()中加入降级判断:
# executor.py (续) async def execute(self, plan: TaskPlan) -> ExecutionResult: # ... 前置校验 try: result_data = await self.tools[plan.task_type](plan.input_data) # 降级判断:仅对 invoice_parse 类型启用置信度检查 if plan.task_type == "invoice_parse": confidence = result_data.get("confidence", 0.0) if confidence < 0.6: return ExecutionResult( plan_id=plan.plan_id, status="partial", output={"fallback_reason": "low_confidence", "screenshot_url": "..."}, tool_usage=[], timestamp=datetime.now() ) elif confidence < 0.85: # 加入人工队列,但不阻塞主流程 await self._enqueue_for_review(plan.plan_id, result_data) return ExecutionResult( plan_id=plan.plan_id, status="success", output=result_data, tool_usage=[...], timestamp=datetime.now() ) except Exception as e: # ... 错误处理5. 避坑:Agentic Design Patterns 开发中 4 条血泪教训
5.1 现象:Planner 生成的plan_id在 Executor 日志里找不到对应记录
原因:Planner 用uuid4()生成 ID,但 Executor 的日志系统启用了loguru的enqueue=True(异步日志),导致plan_id字符串被截断(如a1b2c3d4-e5f6-7890-g1h2-i3j4k5l6m7n8变成a1b2c3d4-e5f6-7890-g1h2-i3j4k5l6m7n)。根本原因是loguru的异步队列缓冲区默认 1024 字节,而 UUIDv4 字符串占 36 字节,但加上日志前缀后超限。
解决:在loguru配置中显式增大enqueue缓冲区:logger.add("file.log", enqueue=True, buffer_size=4096);或更优解——在 Planner 中用shortuuid生成 12 位 ID(shortuuid.uuid()),既保证唯一性又规避截断。
5.2 现象:Executor 执行成功,但 Planner 收到status="failed"
原因:Planner 和 Executor 部署在不同 Docker 容器,时区不一致。Executor 用datetime.now()生成timestamp,而 Planner 的 Pydantic 校验器设置了coerce为True,导致2024-05-20T14:22:00+08:00被强制转为 UTC 时间2024-05-20T06:22:00+00:00,与 Planner 本地时间比较时触发ValueError。
解决:所有时间字段强制用 UTC:Executor 中timestamp=datetime.now(timezone.utc);Planner 的 Pydantic Model 中添加@validator('timestamp')确保入参为 UTC;通信协议文档首行注明 “All timestamps MUST be ISO8601 UTC”。
5.3 现象:多 Executor 并发执行同一plan_id,结果被覆盖
原因:Executor 用文件系统存中间结果(如/tmp/{plan_id}.json),但未加文件锁。两个进程同时open(..., 'w')导致后写入者覆盖先写入者。
解决:弃用文件系统,改用 Redis 的SETNX(SET if Not eXists)命令:redis.setex(f"result:{plan_id}", 3600, json.dumps(result.dict()))。若返回False,说明已被其他 Executor 占用,当前进程立即返回status="conflict"。
5.4 现象:Planner 的 LLM 调用突然全部超时,但 OpenAI Dashboard 显示 API 正常
原因:Planner 使用AsyncOpenAI,但未配置httpx.AsyncClient的连接池参数。默认limits.max_connections=10,当并发请求 >10 时,后续请求在连接池排队,timeout计时从排队开始而非发起请求开始,导致表面超时实为排队超时。
解决:初始化 client 时显式增大连接池:
from httpx import AsyncClient client = AsyncOpenAI( http_client=AsyncClient( limits=httpx.Limits(max_connections=100, max_keepalive_connections=20) ) )6. 把 Agentic Design Patterns 落地为可交付资产:用 Docker Compose 编排双角色服务并注入可观测性
6.1 构建可部署的镜像:分层优化减小体积,避免 runtime 依赖污染
Agentic Design Patterns 的核心价值在于可移植性。我们拒绝“把整个 conda 环境打包进镜像”的粗暴做法,而是用多阶段构建精准控制依赖:
# Dockerfile # 构建阶段:只安装编译依赖 FROM python:3.11-slim AS builder RUN apt-get update && apt-get install -y build-essential libpq-dev && rm -rf /var/lib/apt/lists/* COPY requirements.txt . RUN pip wheel --no-cache-dir --no-deps --wheel-dir /app/wheels -r requirements.txt # 运行阶段:仅复制 wheels 和源码 FROM python:3.11-slim WORKDIR /app # 复制 wheels(不含源码,减小体积) COPY --from=builder /app/wheels /app/wheels # 安装 wheels(--no-index --find-links 避免联网) RUN pip install --no-index --find-links /app/wheels --upgrade pip && \ pip install --no-index --find-links /app/wheels -r requirements.txt # 复制源码(排除测试和文档) COPY --chown=1001:1001 . . USER 1001 CMD ["uvicorn", "main:app", "--host", "0.0.0.0:8000", "--port", "8000"]关键点:
requirements.txt中pydantic==2.7.1显式指定小版本,避免pydantic>=2.0导致不同环境安装不同 minor 版本(V2.6 的RootModel行为与 V2.7 不同,会引发 Schema 校验失败)。
6.2 用 Docker Compose 编排 Planner 和 Executor 服务,并注入 OpenTelemetry
真正的 Agentic Design Patterns 必须可观测。我们不手写日志埋点,而是用 OpenTelemetry 自动捕获 span:
# docker-compose.yml version: '3.8' services: planner: build: . environment: - OTEL_EXPORTER_OTLP_ENDPOINT=http://otel-collector:4317 - OTEL_SERVICE_NAME=planner-service depends_on: - otel-collector executor: build: . environment: - OTEL_EXPORTER_OTLP_ENDPOINT=http://otel-collector:4317 - OTEL_SERVICE_NAME=executor-service depends_on: - otel-collector otel-collector: image: otel/opentelemetry-collector-contrib:0.102.0 command: ["--config=/etc/otel-collector-config.yaml"] volumes: - ./otel-collector-config.yaml:/etc/otel-collector-config.yaml ports: - "4317:4317" # OTLP gRPC - "8888:8888" # Prometheus metrics在planner.py中注入 tracing:
# planner.py (续) from opentelemetry import trace from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter from opentelemetry.sdk.trace import TracerProvider from opentelemetry.sdk.trace.export import BatchSpanProcessor # 初始化 tracer(只在服务启动时执行一次) provider = TracerProvider() processor = BatchSpanProcessor(OTLPSpanExporter()) provider.add_span_processor(processor) trace.set_tracer_provider(provider) # 在 generate_plan 方法中 async def generate_plan(self, user_request: str) -> TaskPlan: tracer = trace.get_tracer(__name__) with tracer.start_as_current_span("planner.generate_plan") as span: span.set_attribute("user_request.length", len(user_request)) # ... LLM 调用逻辑 span.set_attribute("plan_id", plan.plan_id) return plan效果:在 Jaeger UI 中能看到完整链路:
planner.generate_plan→llm.chat.completions.create→executor.execute→log_analyze,每个 span 标注了plan_id、task_type、status,点击即可下钻到具体日志。
6.3 验证模式有效性:用混沌工程注入故障,观测系统韧性
Agentic Design Patterns 的终极验证不是“功能正常”,而是“故障时仍可控”。我们用chaos-mesh注入网络延迟,验证降级策略:
# chaos-network-delay.yaml apiVersion: chaos-mesh.org/v1alpha1 kind: NetworkChaos metadata: name: executor-delay spec: action: delay mode: one selector: namespaces: - default pods: default: - executor-0 delay: latency: "500ms" correlation: "100" duration: "60s"执行后观察指标:
- Planner 的
planner.generate_planP95 从 2.3s 升至 7.8s(因等待 Executor 响应) - 但整体成功率保持 100%,因 Executor 的
log_analyze降级为partial状态,Planner 收到后立即触发人工队列 - Jaeger 中可见
executor.executespan 的status.code为ERROR,但父 spanplanner.generate_plan仍是OK
这证明:模式本身比单个组件更健壮——当 Executor 变慢时,系统没有崩溃,而是按预设契约降级。
我坚持一个习惯:每次新增一个 Agent,必先写它的 failure test case(用 pytest mock 其依赖,强制返回 error),再写 success case。因为 Agentic Design Patterns 的价值,90% 体现在它如何优雅地失败。那些声称“我的 Agent 从不出错”的项目,要么没上生产,要么正躺在监控告警的盲区里。希望帮到你。
本文还有配套的精品资源,点击获取