Skip to content

第五章:类型系统 -- TypeBox 与 Schema 驱动开发

"好的类型系统不是限制你,而是替你思考。"


5.1 为什么需要 Schema?

当 LLM 要调用一个工具时,它需要知道:

  1. 工具叫什么名字
  2. 工具接受什么参数
  3. 每个参数的类型和含义

这些信息就是 Schema——参数的"合同"。

在 Java 中,你可能用过 Jackson 的 @JsonProperty 注解或 Hibernate Validator 来描述数据结构。TypeScript 世界中,TypeBox 做的是同样的事,但更强大——它用代码定义 JSON Schema,同时自动推导出 TypeScript 类型。

📌 源码定位 ───────────────────────────────────── TypeBox 的使用遍布整个项目:

  • packages/agent/src/types.ts -- AgentTool<TParameters extends TSchema>
  • packages/coding-agent/src/core/tools/*.ts -- 每个工具的参数 Schema
  • packages/coding-agent/src/core/extensions/types.ts -- ToolDefinition<TParams>─────────────────────────────────────────────────

5.2 TypeBox 基础

typescript
import { Type, Static } from "typebox";

// 定义 Schema(运行时可用,发送给 LLM)
const readSchema = Type.Object({
    path: Type.String({ description: "Absolute or relative path to the file" }),
    offset: Type.Optional(Type.Number({ description: "Line number to start reading from" })),
    limit: Type.Optional(Type.Number({ description: "Number of lines to read" })),
});

// 从 Schema 推导 TypeScript 类型(编译时可用)
type ReadInput = Static<typeof readSchema>;
// 等价于:
// type ReadInput = {
//     path: string;
//     offset?: number;
//     limit?: number;
// }

⚡ Java 对照 ─────────────────────────────────────

TypeBoxJava 等价
Type.Object({path: Type.String()})class ReadInput { String path; } + @JsonSchema
Type.Optional(Type.Number())@Nullable Integer offset
Type.String({description: "..."})@Schema(description = "...")
Static<typeof schema>编译时生成的类型(无直接等价)

─────────────────────────────────────────────────

5.3 Schema 的双重生命

TypeBox 的核心价值在于一个定义,两种用途

typescript
const echoSchema = Type.Object({
    message: Type.String({ description: "Message to echo" }),
});

// 用途 1:编译时类型安全(给开发者用)
type EchoInput = Static<typeof echoSchema>;
function processInput(input: EchoInput) {
    console.log(input.message);  // TypeScript 知道 message 是 string
}

// 用途 2:运行时 JSON Schema(给 LLM 用)
// echoSchema 的值就是标准 JSON Schema:
// {
//     type: "object",
//     properties: {
//         message: { type: "string", description: "Message to echo" }
//     },
//     required: ["message"]
// }

★ Insight ─────────────────────────────────────这就是"单一事实来源"(Single Source of Truth)原则。 在 Java 中,你可能需要同时维护一个 POJO 类和一个 JSON Schema 文件,它们之间没有自动同步机制。TypeBox 让你只写一次定义,同时得到类型安全的 TypeScript 类型和 LLM 能理解的 JSON Schema。如果你改了 Schema,TypeScript 类型自动更新,编译器会告诉你哪里出了问题。 ─────────────────────────────────────────────────

5.4 工具定义的完整模式

让我们以 Pi 的 read 工具为例,看完整的定义模式:

typescript
// packages/coding-agent/src/core/tools/read.ts(简化)

import { Type, type Static } from "typebox";

// Step 1: 定义 Schema
const readSchema = Type.Object({
    path: Type.String({
        description: "Absolute or relative path to the file to read"
    }),
    offset: Type.Optional(Type.Number({
        description: "Line number to start reading from. Only provide if the file is too large to read at once.",
        minimum: 0
    })),
    limit: Type.Optional(Type.Number({
        description: "Number of lines to read. Only provide if the file is too large to read at once.",
        exclusiveMinimum: 0
    })),
});

// Step 2: 推导输入类型
type ReadToolInput = Static<typeof readSchema>;

// Step 3: 定义结果详情类型(给 UI 用)
interface ReadToolDetails {
    truncation?: { startLine: number; endLine: number; totalLines: number };
}

// Step 4: 定义操作接口(依赖注入,用于测试和远程文件系统)
interface ReadOperations {
    readFile: (absolutePath: string) => Promise&lt;Buffer&gt;;
    access: (absolutePath: string) => Promise<void>;
}

// Step 5: 创建工具定义
function createReadToolDefinition(
    cwd: string,
    options?: { operations?: ReadOperations }
): ToolDefinition<typeof readSchema, ReadToolDetails | undefined> {
    const ops = options?.operations ?? defaultOperations;

    return {
        name: "read",
        label: "Read",
        description: "Read a file from disk",
        parameters: readSchema,              // Schema 发送给 LLM

        async execute(toolCallId, params, signal, onUpdate) {
            // params 的类型是 Static<typeof readSchema> = ReadToolInput
            // TypeScript 自动提供类型安全!
            const absolutePath = path.resolve(cwd, params.path);
            await ops.access(absolutePath);
            const buffer = await ops.readFile(absolutePath);
            const content = buffer.toString("utf-8");

            return {
                content: [{ type: "text", text: content }],
                details: { truncation: { ... } },
            };
        },
    };
}

★ Insight ─────────────────────────────────────5 步定义模式是 Pi 中所有工具的标准写法:Schema → 输入类型 → 详情类型 → 操作接口 → 工具定义。这种模式的好处是每一层都可以独立测试和替换。ReadOperations 接口让你可以注入一个远程文件系统的实现,而不需要修改工具的核心逻辑。这就是依赖注入在函数式风格下的表现形式。 ─────────────────────────────────────────────────

5.5 TypeBox 类型速查表

Pi 中常用的 TypeBox 类型:

typescript
// 基础类型
Type.String()                    // string
Type.Number()                    // number
Type.Boolean()                   // boolean
Type.Null()                      // null

// 带约束
Type.String({ minLength: 1 })    // string & length >= 1
Type.Number({ minimum: 0 })      // number & >= 0
Type.String({ pattern: "^\\d+$" }) // 正则约束

// 可选
Type.Optional(Type.String())     // string | undefined

// 数组
Type.Array(Type.String())        // string[]
Type.Array(Type.Object({...}))   // {name: string}[]

// 对象
Type.Object({
    name: Type.String(),
    age: Type.Optional(Type.Number()),
})

// 联合
Type.Union([Type.String(), Type.Number()])  // string | number

// 字面量
Type.Literal("read")             // "read" (字面量类型)

⚡ Java 对照 ─────────────────────────────────────

TypeBoxJava Bean Validation
Type.String({minLength: 1})@NotBlank
Type.Number({minimum: 0})@Min(0)
Type.Optional(...)@Nullable
Type.Array(...)List<...>
Type.Object({...})@Data class

─────────────────────────────────────────────────

5.6 validateToolArguments:运行时验证

TypeBox 不仅定义类型,还能在运行时验证数据:

typescript
// packages/ai 中的验证函数
import { validateToolArguments } from "@mariozechner/pi-ai";

const tool = { parameters: readSchema, ... };
const rawArgs = { path: "/etc/passwd", offset: -1 };  // 可能来自 LLM 的不可信输入

// 运行时验证(会检查 minimum: 0 约束)
const validated = validateToolArguments(tool, { id: "call_1", name: "read", arguments: rawArgs });
// 如果 offset 不满足 minimum: 0,会抛出错误

★ Insight ─────────────────────────────────────为什么要运行时验证? 因为 LLM 的输出是不可信的。LLM 可能返回不合法的参数(如负数行号、不存在的路径)。TypeBox 的 Schema 验证就像 Java 的 Bean Validation——在数据进入业务逻辑之前拦截非法值。这是防御性编程的关键一环。 ─────────────────────────────────────────────────

5.7 泛型约束:TSchema 的作用

为什么工具定义要用 TParameters extends TSchema 而不是直接用 any

typescript
// 这样写:泛型约束确保类型安全
interface AgentTool&lt;TParameters extends TSchema&gt; {
    parameters: TParameters;
    execute: (params: Static&lt;TParameters&gt;) => Promise&lt;AgentToolResult&gt;;
}

// 使用时:
const readTool: AgentTool<typeof readSchema> = { ... };
readTool.execute({ path: "/etc/passwd" });     // OK
readTool.execute({ path: 123 });               // 编译错误!path 必须是 string
readTool.execute({ foo: "bar" });              // 编译错误!没有 foo 字段

如果用 any 代替 TSchema,你将失去所有类型检查。泛型约束在这里的作用就像 Java 的 <T extends Comparable<T>>——它让编译器帮你检查正确性。

5.8 satisfies 操作符

Pi 中一个常见的模式是使用 satisfies

typescript
const DEFAULT_MODEL = {
    id: "unknown",
    name: "unknown",
    api: "unknown",
    provider: "unknown",
    baseUrl: "",
    reasoning: false,
    input: [],
    cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
    contextWindow: 0,
    maxTokens: 0,
} satisfies Model<any>;

satisfiesas 的区别:

  • as Model<any> -- 强制转换,可能隐藏错误
  • satisfies Model<any> -- 检查是否满足类型,但保留原始字面量类型

★ Insight ─────────────────────────────────────satisfies 是 TypeScript 4.9 引入的,是类型安全的"验证但不转换"。 在 Java 中没有直接等价物。它相当于说:"请验证这个对象满足 Model<any> 的所有要求,但不要忘记它具体的字段值。" 这样你后续访问 DEFAULT_MODEL.id 时,TypeScript 知道它的值是 "unknown" 而不仅仅是 string─────────────────────────────────────────────────

5.9 本章小结

TypeBox 在 Pi 中扮演的角色:

定义 Schema          →  Type.Object({ ... })

    ├── 编译时        →  Static<typeof schema>  (TypeScript 类型)
    │                   用于代码中的类型检查

    └── 运行时        →  schema 本身 (JSON Schema)
                        ├── 发送给 LLM(工具参数描述)
                        └── 验证 LLM 返回的参数

核心价值:一个定义,两种用途,零重复。


第四章:消息体系 | 第六章:Agent Core -- 引擎的心脏 →

基于 MIT 许可证发布