乐于分享
好东西不私藏

AI 工具链边界怎么建?两层门控 + 判断执行分离 + Zod 输出验证

AI 工具链边界怎么建?两层门控 + 判断执行分离 + Zod 输出验证

上篇说,AI 工具链越用越乱的根源不是工具选错了,是工程系统没有边界。

让 AI 工具链越用越乱的,不是工具,是你缺了系统边界

三类症状我拆完了:输入没有边界、执行没有边界、输出没有边界。

但"有边界"和"没边界",代码里长什么样?

这篇我用一个可以跑通的 Demo 把三类边界的工程实现拆出来。


Demo 场景

一个工程报告分类 Agent。

工程师提交一条问题报告,Agent 负责:结构化提取信息 → 判断是否可操作 → 分类并给出处理建议。

技术栈:TypeScript + OpenAI SDK(接 DeepSeek)+ Zod。场景很普通,但足以演示三类边界的完整结构。


输入边界:两层门控,不满足就不进 AI

输入边界的核心问题是:什么样的输入才能进入 AI 处理流程?

答案不是"有内容就行",而是"满足结构要求才行"。

我的实现分两层。

第一层:前置规则检查(不调用 AI)

在任何 API 调用之前,先做基本验证。这些检查不消耗 token,不满足直接拒绝:

if (!rawText || rawText.trim().length < 20) {  throw new InputBoundaryError(    `输入过短(当前 ${rawText.trim().length} 字符,最少 20 字符)`  )}

第二层:AI 提取后的 Zod 验证

让 AI 把原始文本提取成结构化字段,然后用 Zod 验证提取结果是否符合 schema:

const result = StructuredReportSchema.safeParse(raw)if (!result.success) {  const missingFields = result.error.errors.map((e) => e.path.join(&#x27;.&#x27;))  throw new InputBoundaryError(    `结构化提取失败,以下字段缺失或无效:${missingFields.join(&#x27;, &#x27;)}`  )}

Schema 明确了哪些字段必须有、允许哪些值:

export const StructuredReportSchema = z.object({  problemType:       z.enum([&#x27;bug&#x27;, &#x27;performance&#x27;, &#x27;security&#x27;, &#x27;question&#x27;, &#x27;feature&#x27;]),  severity:          z.enum([&#x27;critical&#x27;, &#x27;high&#x27;, &#x27;medium&#x27;, &#x27;low&#x27;]),  affectedSystem:    z.string().min(1),  description:       z.string().min(10),  reproductionSteps: z.string().nullable(),  context:           z.string().nullable(),})

实际跑起来,"接口有时候报错。"这样的输入——8 个字符,直接在第一层被拦下,提取 prompt 根本没有发出去:


执行边界:判断层和执行层,必须分开

这是三类边界里最容易被忽视的一个。

常见的做法是把"判断能不能处理"和"决定怎么处理"写在同一个 prompt 里。逻辑上没问题,但一旦出了问题,你根本定位不了:是判断错了,还是决策错了?

我的做法是把两者完全拆开。

判断层:只负责一件事——这份报告有没有足够信息可以被处理?

content: `你是分流判断器。你的唯一职责是判断这份工程报告是否有足够信息可以被分类处理。返回 JSON:{ "actionable": true | false, "reason": "...", "missingInfo": [...] }`

执行层:只在判断层确认 actionable=true 之后才被调用,只负责分类决策:

content: `你是分类决策引擎。给定一份已验证的、可操作的工程报告,输出处理决策。`

两个独立的 LLM call,各自的 prompt 只做一件事。

直接的好处是:哪一层出了问题,立刻能定位。判断层返回 actionable=false 时,执行层根本不会被调用。

实际跑起来,"交易平台性能下降,但不知道具体在哪"这样的报告——输入边界通过了(能提取出结构化字段),判断层给出明确的拒绝理由,执行层没被触发:

这里有个值得注意的细节:severity 字段被 AI 猜成了 medium,Zod 没有拒绝它,输入边界通过了。但判断层没有放行。

输入边界回答的是"结构合不合法",判断层回答的是"信息够不够"。两个问题不同,两层各司其职。如果把这两层合并,就分不清是哪个维度出了问题。


输出边界:不验证,就没有可靠性

AI 的输出格式不是永远可信的。

没有输出边界,工具链就建立在"AI 大概会按格式输出"这个假设上。这个假设会在某个时刻失效,然后你会在下游某个地方看到一个奇怪的报错,不知道从哪里来。

我的做法是用 Zod 对执行层的输出做强校验:

const result = ClassificationResultSchema.safeParse(raw)if (!result.success) {  throw new OutputBoundaryError(    `输出被拒绝,schema 校验失败:${violations.join(&#x27;; &#x27;)}`  )}

Schema 定义了所有允许的值域:

export const ClassificationResultSchema = z.object({  category:        z.enum([&#x27;incident&#x27;, &#x27;task&#x27;, &#x27;investigation&#x27;, &#x27;documentation&#x27;, &#x27;wont-fix&#x27;]),  priority:        z.enum([&#x27;P0&#x27;, &#x27;P1&#x27;, &#x27;P2&#x27;, &#x27;P3&#x27;]),  suggestedAction: z.string().min(1).max(200),  estimatedEffort: z.enum([&#x27;< 1h&#x27;, &#x27;1-4h&#x27;, &#x27;4-8h&#x27;, &#x27;> 1 day&#x27;]),  shouldEscalate:  z.boolean(),})

通不过验证,直接拒绝,不流入下游。下游收到的永远是结构确定的数据,而不是"大概对"的内容。


三类边界跑通之后

信息清晰的报告,四关全部通过,直接拿到分类结果:

三类边界建立之后,这个 Agent 有一个明显的特征:

出了问题,能知道在哪里出的。

输入边界拒绝了,是原始数据不满足条件。判断层拒绝了,是信息不够充分。执行层的输出被输出边界拒绝,是 AI 没有按格式输出。每一层的职责是清楚的,每一层的拒绝理由是可追踪的。

这和没有边界时完全不同。没有边界时,出了问题,你不知道是哪个环节错了,只能从头找。


边界不是在限制 AI。

边界是在给每一个组件一个清晰的工位:你负责什么,你拒绝什么,你的输出长什么样。

工位清楚了,工具链才能稳定运转。


如果这篇对你有用,点个 ❤️ 推荐给更多工程人。

关注「架构师的AI实验室」,持续记录 AI 进入工程系统的真实过程。