
为什么 Agent 需要结构化输入
截图对程序来说只是一堆像素,路由逻辑没法直接拿它写 if。Agent 真正要的是产品名、时间、时长这类有明确类型的值。
Clairic 模型页里的「Agent 前置步骤」说的就是这件事:先把关键值结构化,再让 Agent 根据这些值做路由或判断。
定义字段(defineFields)
用 defineFields 加字段工厂函数描述你要的东西。本例用到 textField、dateTimeField 和 durationField({ unit: 'minutes' }),每个字段指什么,写在 instructions 里。
字段名需符合 [A-Za-z_][A-Za-z0-9_.-]{0,63}。
| 字段 | 类型 | 含义 |
|---|---|---|
product | text | 截图里报错的产品名 |
error_time | datetime | 报错发生的日期时间 |
impact | duration(分钟) | 影响持续了多久 |
发送图片(ImageInput,state 留空)
ImageInput 有两种写法:{ mediaType, base64 },或者 base64 data URL。传了图片,state 就可以是 ''。远程图片 URL 会被拒绝,所以要先读文件再内嵌。
安装 SDK:
bashnpm install @system-one-ai/core@0.8.0 @system-one-ai/adapter-system-one@0.7.0 @system-one-ai/transport-fetch@0.8.0
保存为 screenshot-to-json.mts,在 .env 里设置 SYSTEM_ONE_API_KEY:
tsimport { readFile } from 'node:fs/promises';
import {
createSystemOne, defineFields, textField, dateTimeField, durationField,
type ImageInput,
} from '@system-one-ai/core';
import { systemOneAdapter } from '@system-one-ai/adapter-system-one';
import { createFetchTransport } from '@system-one-ai/transport-fetch';
const apiKey = process.env.SYSTEM_ONE_API_KEY;
const imagePath = process.argv[2];
if (!apiKey || !imagePath) throw new Error('Set SYSTEM_ONE_API_KEY and supply a PNG path.');
const images: readonly ImageInput[] = [{
mediaType: 'image/png',
base64: (await readFile(imagePath)).toString('base64'),
}];
const client = createSystemOne({
adapter: systemOneAdapter,
transport: createFetchTransport(),
apiKey,
timeoutMs: 30_000,
maxRetries: 0,
});
const fields = defineFields({
product: textField({ instructions: 'The product or service named in the error screenshot.' }),
error_time: dateTimeField({ instructions: 'When the error happened, as shown in the screenshot.' }),
impact: durationField({ unit: 'minutes', instructions: 'How long the problem has lasted.' }),
});
const result = await client.extract({
model: 'clairic',
state: '',
images,
fields,
});
console.log({
model: result.model,
product: result.answers.product.value,
errorTime: result.answers.error_time.value,
impactMinutes: result.answers.impact.value,
impactPNull: result.answers.impact.pNull,
truncated: result.truncated,
usage: result.usage,
});
运行:node --env-file=.env screenshot-to-json.mts ./error.png。注意每运行一次,就会产生一次计费请求。

读结果:value、confidence、pNull、span
只有 value 是一定会返回的。标量字段在模型给出时,还会带 guess、confidence 和 pNull。pNull 超过 null 阈值(默认 0.5)时,value 为 null,guess 里则可能还有值。
输出大致长这样(示意,不是真实结果):
json{
"model": "clairic",
"product": "<product name from the screenshot>",
"errorTime": "<datetime string>",
"impactMinutes": null,
"impactPNull": "<0–1, above the null threshold>",
"truncated": false,
"usage": { "inputTokens": "<n>", "outputTokens": "<n>", "totalTokens": "<n>" }
}
示例截图里没写影响了多久,所以 impact 返回 null,而不是随便猜个数(guess 里可能仍有值)。建议给 confidence、pNull 设好阈值,达不到的转人工。result.model 可能和你请求的模型不同,两个都记下来;truncated 一定是布尔值,usage 里是 token 用量。

| 返回键 | 何时出现 | 含义 |
|---|---|---|
value | 总会有 | 类型化的值,或 null |
guess / confidence / pNull | 标量字段,模型返回时 | 猜测值、置信度、输入确定不了该值的概率 |
span | text 字段,有 state 文本时 | 该值在 state 里的起止字符位置 |
mean / median / p10 / p90 / pBelow / pAbove | 仅 estimate、forecast | 估计值的分布 |
串联 Jev、Clef 做决策
模型页里的「工单信息整理」就是这个思路:Clairic 取出产品名、报错时间、影响时长,再交给 Jev 或 Clef 判断分给哪个团队。接在上面的代码后面:
tsimport { choice } from '@system-one-ai/core';
const decision = await client.evaluate({
model: 'jev',
state: {
product: result.answers.product.value,
error_time: result.answers.error_time.value,
impact_minutes: result.answers.impact.value,
},
questions: {
team: choice('Choose the team that should handle this error report.', {
billing: 'Payments and refunds',
support: 'Technical issues',
review: 'More information is needed',
}),
},
});
console.log(decision.answers.team.choice);
extract() 不传 questions,evaluate() 不传 fields。决策这一步拿到的是结构化的值,而不是原图。这个模式的完整说明见先抽取再决策,决策模型见 Jev。
 → 类型化字段 → Jev evaluate() → 团队。设计制作。)
图片限制
| 限制 | 值 |
|---|---|
| 每次请求图片数 | 最多 4 张 |
| 格式 | PNG、JPEG、WebP |
| 单张大小 | 4MB |
| 图片合计 | 8MB |
| 整个 JSON 请求体 | 不超过 13MB |
| 传法 | 只能 base64,不收远程 URL |
Clairic 按输入大小每次请求收一次费,不按字段数计,费率见 Clairic 模型页。
核验日期:2026-10-22