第2章:Agent 开发的第一性原理:结构化工程

100 阅读5分钟

Agent 开发的第一性原理:结构化工程 (Structured Engineering)

摘要:为什么你的 Agent 经常“胡言乱语”导致程序报错?因为你还在用“自然语言”和它交互。本文将揭示 Agent 开发中最关键的一环:如何通过 SchemaPydantic 将 LLM 从一个“不可控的诗人”变成一个“严谨的程序员”。

1. 痛点:LLM 的“话唠”属性 vs 程序的“严谨”需求

在 Agent 开发的早期,开发者最头疼的问题通常是这样的:

你写了一个 Python 脚本,想要让 AI 帮你提取用户信息,然后存入数据库。 你的 Prompt:

"请从这句话中提取用户的姓名和年龄:'我叫张三,今年25岁',请只返回 JSON 格式。"

LLM 的回答 (A):

{
  "name": "张三",
  "age": 25
}

LLM 的回答 (B) —— (当你再次运行时):

"好的,这是您需要的 JSON 数据:

{ "name": "张三", "age": 25 }

希望这能帮到您!"

结果: 回答 A 能跑通代码;回答 B 直接让你的 json.loads() 抛出异常,程序崩溃。

这就是 Agent 开发的第一性原理:软件工程是确定性的 (Deterministic),而 LLM 是概率性的 (Probabilistic)。结构化工程 (Structured Engineering) 就是在这两者之间架起的一座桥梁。

2. 什么是结构化工程?

结构化工程不再把 LLM 仅仅看作一个“聊天机器人”,而是把它看作一个 “自然语言处理函数”

  • 输入:自然语言 (Prompt)
  • 处理:LLM 推理
  • 输出:严格符合 Schema 定义的数据结构 (JSON/XML/Object)

我们不再乞求模型:“Please give me JSON”,而是强制约束模型:“你必须填充这个类结构”。

3. 核心武器:Schema 与 Pydantic

在 Python 生态中,Pydantic 是定义数据结构的事实标准。在 Agent 开发中,它是我们定义“协议”的神器。

3.1 定义你的“期望”

假设我们要开发一个“订单助手 Agent”,我们需要提取用户的购买意图。

不好的做法 (自然语言):

prompt = "提取订单信息,包括商品名、数量和价格..."

结构化工程的做法 (Code):

from pydantic import BaseModel, Field
from typing import List

class OrderItem(BaseModel):
    product_name: str = Field(..., description="商品的具体名称")
    quantity: int = Field(..., description="购买数量,必须大于0")
    # 甚至可以添加逻辑约束
    notes: str = Field(None, description="用户的特殊备注")

class OrderExtraction(BaseModel):
    items: List[OrderItem]
    total_estimated_price: float = Field(..., description="预估总价")
    shipping_address: str = Field(..., description="配送地址")

看到区别了吗?我们不仅定义了字段名,还定义了类型 (int, str)语义描述 (description) 。这不仅仅是代码,这其实就是传给 LLM 的 Prompt

3.2 强类型驱动 (Type-Driven Development)

当你把这个 Schema 传给支持 Function Calling 或 Structured Output 的模型(如 GPT-4o, Claude 3.5 Sonnet)时,奇迹发生了。

import openai

client = openai.OpenAI()

completion = client.beta.chat.completions.parse(
    model="gpt-4o-2024-08-06",
    messages=[
        {"role": "system", "content": "你是订单处理助手。"},
        {"role": "user", "content": "我要两杯拿铁,少冰,送到科技园A栋,大概多少钱?"},
    ],
    # 核心:直接传入 Pydantic 类
    response_format=OrderExtraction,
)

# 获取结果(自动解析为 Python 对象,不是字典,也不是字符串!)
order_data = completion.choices[0].message.parsed

print(f"地址: {order_data.shipping_address}")
print(f"商品: {order_data.items[0].product_name}")
print(f"备注: {order_data.items[0].notes}")

输出结果:

地址: 科技园A栋
商品: 拿铁
备注: 少冰

4. 为什么这比 Prompt Engineering 更重要?

4.1 消除“幻觉”与“废话”

通过 Schema 约束,模型不可能输出“好的,这是您的数据...”这种废话,因为它必须严格符合 JSON Schema 结构。

4.2 自动类型验证 (Validation)

如果用户说:“我要买 -5 个苹果”。

  • 普通 Prompt:LLM 可能会输出 {"quantity": -5},导致后端逻辑错误。
  • 结构化工程:Pydantic 会在解析阶段直接抛出 ValidationError。我们可以捕获这个错误,让 Agent 意识到输入有问题,甚至自动进行 Self-Correction (自我修正)

4.3 思维链的结构化 (Structured CoT)

我们甚至可以将“思考过程”结构化。

class ReasonedResponse(BaseModel):
    thought_process: str = Field(..., description="逐步分析用户的意图和潜在问题")
    final_answer: str = Field(..., description="给用户的最终回复")

这样,我们强制模型在给出答案前,必须先填写 thought_process 字段。这实际上是将 Chain-of-Thought (思维链) 固化在了代码层面,极大地提升了复杂任务的准确率。

5. 进阶:从“输出”到“流程控制”

结构化工程不仅仅用于提取数据,它还是 Agent 路由 (Routing) 的基础。

想象一个客服 Agent,需要决定是将问题转给“技术支持”还是“销售部门”。

from enum import Enum

class Department(str, Enum):
    SALES = "sales"
    TECH_SUPPORT = "tech_support"
    REFUND = "refund"

class Router(BaseModel):
    reasoning: str = Field(..., description="分析用户诉求")
    department: Department = Field(..., description="应该分发的部门")

通过限制 department 必须是 Enum 中的一个值,我们彻底根除了 Router 可能会路由到一个不存在部门的风险。

6. 总结

在 Agent 开发实战中,请记住以下三条法则:

  1. 不要解析字符串:永远不要用正则表达式去从 LLM 的回复中抓数据。
  2. Schema 即 Prompt:Pydantic 的字段名和 Description 就是最高效的 Prompt。
  3. 一切皆对象:输入是对象,输出是对象,工具的参数也是对象。

结构化工程是将 AI 的“创造力”关进“工程化”笼子里的那把锁。掌握了它,你就掌握了构建稳定、可靠 Agent 系统的钥匙。