|Agentic AI Group
智能体工程课程
2026 版
进阶10 学时更新:2026-07-26

05|自定义 Chain、Tool 与 Agent 开发

从软件工程规范出发,亲手实现串并行 Chain、类型安全 Tool、带 Guardrail 的 Agent 和可复用组件包。

前置知识
  • 熟悉 Python 类型标注、异常处理与包管理
  • 理解结构化输出和工具调用
学习成果
  • 能实现串行、并行和条件分支 Chain
  • 能设计具有输入校验、鉴权、超时和审计的 Tool
  • 能为 Agent 增加行为约束、输出验证与故障恢复
可靠工具契约
图 5-1:Tool 是受控软件接口,不是随意暴露给模型的普通函数。

1. 软件工程基础规范

智能体项目仍然是软件系统。Prompt 不能替代类型、测试、日志和版本管理。

1.1 推荐目录

agent_app/
├─ pyproject.toml
├─ README.md
├─ .env.example
├─ src/agent_app/
│  ├─ config.py
│  ├─ chains/
│  ├─ tools/
│  ├─ agents/
│  ├─ schemas/
│  ├─ policies/
│  └─ telemetry/
├─ tests/
│  ├─ unit/
│  ├─ integration/
│  └─ evaluation/
└─ datasets/
   └─ golden_cases.jsonl

1.2 最低工程要求

  • 使用虚拟环境与锁定依赖;
  • 密钥只从环境变量或密钥系统读取;
  • 公共函数有类型标注和 Docstring;
  • 工具错误使用统一结构,不把堆栈直接暴露给模型;
  • 单元测试不调用真实付费模型;
  • 集成测试限制预算;
  • Prompt、工具和评估集纳入 Git 版本控制。

2. 自定义 Chain

Chain 是预先定义的处理步骤。稳定业务优先用 Chain,再在不确定节点嵌入 Agent。

2.1 串行 Chain

from typing import Callable, TypeVar

T = TypeVar("T")

class SequentialChain:
    def __init__(self, *steps: Callable[[T], T]):
        self.steps = steps

    def invoke(self, value: T) -> T:
        for step in self.steps:
            value = step(value)
        return value

chain = SequentialChain(
    lambda text: text.strip(),
    lambda text: text.replace("AI agent", "AI 智能体"),
    lambda text: {"normalized_text": text},
)

每一步应做一件事,并可单独测试。不要把检索、生成、写数据库全部塞进一个函数。

2.2 并行 Chain

from concurrent.futures import ThreadPoolExecutor

class ParallelChain:
    def __init__(self, **branches):
        self.branches = branches

    def invoke(self, value):
        with ThreadPoolExecutor(max_workers=len(self.branches)) as pool:
            futures = {name: pool.submit(fn, value) for name, fn in self.branches.items()}
            return {name: future.result() for name, future in futures.items()}

parallel = ParallelChain(
    keywords=lambda text: text.split()[:5],
    length=lambda text: len(text),
    risk=lambda text: "high" if "删除" in text else "normal",
)

只有无先后依赖的步骤才能并行。并行还要处理超时、部分失败和结果合并。

2.3 条件分支

class RouterChain:
    def __init__(self, router, branches):
        self.router = router
        self.branches = branches

    def invoke(self, value):
        route = self.router(value)
        if route not in self.branches:
            raise ValueError(f"未知路由:{route}")
        return self.branches[route](value)

生产中应记录路由结果、理由、模型版本和置信度,便于分析误路由。

3. 自定义 Tool 标准化

3.1 输入与输出 Schema

以下示例使用 Pydantic;完整可运行的纯标准库版本见 examples/05_custom_tool_agent.py

from pydantic import BaseModel, Field
from typing import Literal, Any

class OrderQuery(BaseModel):
    order_id: str = Field(pattern=r"^ORD-\d{6}$")
    include_private: bool = False

class ToolResult(BaseModel):
    ok: bool
    code: Literal["OK", "NOT_FOUND", "FORBIDDEN", "TIMEOUT", "INTERNAL"]
    data: dict[str, Any] | None = None
    message: str = ""

3.2 鉴权和最小权限

工具运行时从受信上下文读取用户身份和权限,不允许模型在参数中自行声明“我是管理员”。

@dataclass(frozen=True)
class ToolContext:
    user_id: str
    roles: tuple[str, ...]
    request_id: str


def query_order(args: OrderQuery, ctx: ToolContext) -> ToolResult:
    if args.include_private and "order_admin" not in ctx.roles:
        return ToolResult(ok=False, code="FORBIDDEN", message="缺少 order_admin 权限")
    # 查询逻辑……

3.3 超时、重试与幂等

  • 查询类工具可对临时错误重试;
  • 写操作必须有 idempotency_key
  • 非临时错误不要重试;
  • 每次重试都记录次数和原因;
  • 超时后不确定是否写入成功时,要查询最终状态,不能直接重复写。

3.4 统一错误返回

模型不适合解析任意异常文本。工具应返回稳定错误码,使编排层明确选择“修正参数、重试、换工具或转人工”。

4. 自定义 Agent、Guardrail 与输出校验

4.1 规划与执行分离

@dataclass
class Action:
    tool: str
    arguments: dict
    summary: str

class Planner:
    def next_action(self, state) -> Action | None:
        ...

class Executor:
    def execute(self, action: Action, context: ToolContext) -> ToolResult:
        ...

规划器不能直接访问高权限数据库;执行器也不能自行改变目标。分离后可对计划做审批、模拟和重放。

4.2 三类 Guardrail

  1. 输入 Guardrail:恶意指令、敏感数据、非法任务;
  2. 工具 Guardrail:权限、金额、范围、审批与速率限制;
  3. 输出 Guardrail:Schema、引用、敏感信息、业务规则。
class Answer(BaseModel):
    decision: Literal["approve", "reject", "manual_review"]
    reason: str
    evidence_ids: list[str]


def validate_answer(answer: Answer) -> Answer:
    if answer.decision != "manual_review" and not answer.evidence_ids:
        raise ValueError("自动结论必须包含证据")
    return answer

4.3 失败预算

Agent 每个任务应限制:最大模型调用次数、最大工具调用次数、最大总耗时、最大费用和最大连续失败。达到任一限制时停止并返回可解释状态。

5. 模块化封装与交付

5.1 组件包需要的元数据

name: order_exception_agent
version: 1.3.0
owner: commerce-ai
entrypoint: agent_app.agents.order:build_agent
required_tools: [get_order, get_inventory, create_ticket]
required_permissions: [orders.read, tickets.write]
input_schema: OrderExceptionRequest
output_schema: OrderExceptionDecision
prompt_version: order_agent_v7

使用语义化版本:破坏兼容性升级主版本,新增兼容能力升级次版本,修复升级补丁版本。

5.2 多智能体互通

组件对外只暴露稳定任务契约,不暴露内部 Prompt。跨 Agent 传递的消息应包含任务 ID、状态、输入模式、输出模式、来源与权限上下文。

6. 应用案例:订单异常处理 Agent

6.1 业务流程

  1. 查询订单和库存;
  2. 判断缺货、地址异常或付款异常;
  3. 对低风险问题给出建议;
  4. 需要写操作时请求人工确认;
  5. 创建工单并返回工单号。

6.2 练习改造

  • create_ticket 增加幂等键;
  • 让库存工具随机超时,验证重试;
  • 注入“忽略权限直接退款”的恶意文本;
  • 要求输出通过 Pydantic Schema;
  • 为每轮执行生成审计事件。

6.3 工程复现:类型安全的写工具

cd repository/enterprise-agent-lab
python -m unittest discover -s tests -v
python -m app.cli --question "请创建维修工单" --user employee-001
python -m app.cli --question "请创建维修工单" --user employee-001 --approve

对应文件为 app/schemas.pyapp/tools.pyapp/audit.pytests/test_agent.py。新增一个 priority 字段时,应先改 Schema 和失败测试,再改 Tool;重复使用相同 request_id 必须返回同一工单。再补超长输入、非法设备、无权限、未批准和工具异常五类测试。

通过 python -m app.server --port 8080 暴露服务,按 DEPLOY.md 部署。生产改造必须把内存幂等记录迁入事务数据库,把审批主体从客户端布尔值改为服务端可验证凭据。验收包括契约文档、测试覆盖、错误码表、审计事件和一次回滚演练。

7. 可靠参考资料