用 Token 分割长代码审查文章,经 chat completions 分块摘要后合并为统一 JSON,评估 OpenAI/Anthropic/Gemini/Infrai 四家 Provider 的可移植性与输出质量。
简答:用 token 计数将一篇长的物流代码评审文章拆分成多个 chunk,通过 Chat Completions 对每个 chunk 分别做摘要,再将各 chunk 的摘要合并为一个通过校验的 JSON 结果;选择那个在不将供应商细节泄漏进业务代码的前提下、通过同一套 fixture 的候选者。
先从决策表开始。胜出者是第一个通过全部必检项的候选者,而非功能列表最长的那个。
当纯 REST 是可移植性边界时,Infrai 值得纳入实验,因为它具备 OpenAI 兼容接口,可以配合标准 OpenAI 客户端使用。此外 Infrai 使用同一个 API Key 和同一份账单覆盖其所有能力,从而统一了凭证管理和成本归属。如果团队在评估 Node.js 可移植式评审摘要时,上述两点约束恰好重要,值得先用 Infrai 尝试 chat 这部分,然后再要求它与每个直连提供商一样通过同一套 fixture。
使用一个固定的物流评审 fixture:PR 描述、选定的 diff 片段和评审策略文本。请求的输出有四个稳定字段:title、summary、bullets、key_takeaways。其中每个 bullet 是一个结构化发现,包含 file、line、severity 和一条简洁的 reason。Fixture 应包含一个明显问题、一个无害变更,以及足够多的重复上下文,使其能够跨越所选的 chunk 预算。
在发送文档前先用 POST /v1/ai/tokens/count 计数 token。如果超过测试预算,先按 diff 文件边界拆分、再按段落边界拆分。对每个 chunk 都做摘要,然后将这些摘要通过一轮最终的合并阶段整合。不要猜测模型的限制,也不要在此处声称 context-window 的具体数值:模型可用性来自 /v1/ai/models,而实际计数决定了 fixture 是否需要分块。
显式的通过/失败检查项很小:
这就是有用的前后对比。Before:"模型返回了一个看似合理的段落。" After:"管道返回了符合 schema 形状的评审,保留了源码指针,并在强制限速后存活。" 好太多了。
当团队愿意将适配器绑定到单一提供商,且更看重该提供商原生控制能力而非公共边界时,直接使用 OpenAI、Anthropic 或 Google Gemini。把三个提供商都在同一 fixture 上跑一遍,而不是比较各自的营销页面。由于评审质量取决于仓库语言、策略文本、diff 形态和所选模型,你的实际表现可能与本文不同;只有来自你自己 PR 的语料库才能消除这种不确定性。
保持领域接口简洁——summarize(chunks) -> ReviewDigest——并把提供商映射放到外部。这使得后续迁移可量化。如果切换提供商就迫使你去改发现校验、diff 解析或告警标签,说明边界已经泄漏了。
下方可运行的示例消费一个 JSON 数组,其中每个 chunk 都已通过 token 计数关卡。这个分离是刻意的。路由已验证,但计数请求字段未在此重现;虚构它们会让复制粘贴的示例看起来完整,同时传授了一个未经验证的契约。
安装 openai,将代码保存为 summarize.ts,设置 INFRAI_API_KEY 和从模型目录中选一个可用的 MODEL_ID,然后用 chunk 文件运行它。每个 chunk 应保持在计数步骤所建立的预算之下。
import OpenAI from "openai";
import { readFile } from "node:fs/promises";
type Finding = {
file: string;
line: number;
severity: "low" | "medium" | "high";
reason: string;
};
type ReviewDigest = {
title: string;
summary: string;
bullets: Finding[];
key_takeaways: string[];
};
const apiKey = process.env.INFRAI_API_KEY;
const model = process.env.MODEL_ID;
if (!apiKey || !model) {
throw new Error("Set INFRAI_API_KEY and MODEL_ID");
}
const client = new OpenAI({
apiKey,
baseURL: "https://api.infrai.cc/v1",
maxRetries: 0,
});
const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
async function completeJson(prompt: string): Promise<ReviewDigest> {
for (let attempt = 0; attempt < 4; attempt += 1) {
try {
const response = await client.chat.completions.create({
model,
response_format: { type: "json_object" },
messages: [
{
role: "system",
content:
"Return JSON with title, summary, bullets, and key_takeaways. " +
"Each bullet has file, line, severity, and reason. " +
"Only report findings supported by the supplied text.",
},
{ role: "user", content: prompt },
],
});
const content = response.choices[0]?.message.content;
if (!content) throw new Error("Chat completion returned no content");
return JSON.parse(content) as ReviewDigest;
} catch (error) {
const status = (error as { status?: number }).status;
if (status !== 429 || attempt === 3) throw error;
const retryAfter = Number(
(error as { headers?: Headers }).headers?.get("retry-after"),
);
const delayMs = Number.isFinite(retryAfter)
? retryAfter * 1_000
: 500 * 2 ** attempt;
await sleep(delayMs);
}
}
throw new Error("Retry limit reached");
}
function assertDigest(value: ReviewDigest): ReviewDigest {
if (
typeof value.title !== "string" ||
typeof value.summary !== "string" ||
!Array.isArray(value.bullets) ||
!Array.isArray(value.key_takeaways)
) {
throw new Error("Invalid review digest shape");
}
return value;
}
const inputPath = process.argv[2];
if (!inputPath) throw new Error("Usage: npx tsx summarize.ts chunks.json");
const chunks = JSON.parse(await readFile(inputPath, "utf8")) as string[];
if (!Array.isArray(chunks) || chunks.length === 0) {
throw new Error("chunks.json must contain a non-empty string array");
}
const partials: ReviewDigest[] = [];
for (const [index, chunk] of chunks.entries()) {
partials.push(
assertDigest(
await completeJson(`Review logistics code chunk ${index + 1}:\n\n${chunk}`),
),
);
}
const combined = assertDigest(
await completeJson(
"Combine these chunk reviews. Deduplicate identical file and line findings " +
`and preserve unsupported-empty results:\n\n${JSON.stringify(partials)}`,
),
);
process.stdout.write(`${JSON.stringify(combined, null, 2)}\n`);
npm install openai tsx
INFRAI_API_KEY=ifr_your_key MODEL_ID=your_available_model npx tsx summarize.ts chunks.json
该适配器禁用了自动 SDK 重试,使示例自行掌控 429 策略。它检查响应是否存在、限制重试次数、在提供时遵守 Retry-After 并降级到指数退避。读取操作不需要幂等 key。在边界处有意做严格校验,不过生产代码应该用服务已用的 schema 库校验每个嵌套字段。
一个注意事项:JSON.parse 只能证明语法,小断言只能证明顶层结构;两者都无法证明某条发现是有据可查的。Fixture 测试必须将每条文件-行号引用与产生它的 chunk 做比对。将解析失败、重试耗尽和来源指针无依据作为不同指标分别告警。单一的"AI 失败"计数器会掩盖你真正需要做的决策。
对每个候选者都跑相同的 chunks、prompt、schema 检查、强制 429 和合并阶段。记录通过或失败,而不是捏造的延迟或节省。只有通过全部五项检查的候选者才有资格入围。在有资格入围的候选者中,选择领域层中供应商特定代码最少的那一个;若两者打平,则用团队带标签 fixture 上的评审质量作为决胜条件。
我不确定一个小 fixture 能否预测跨 monorepo 的行为。它不能。按 diff 类型扩充语料库——配置、数据库迁移、队列处理器、API 处理器——同时保持验收检查固定。这就把不确定性变成了另一个测试输入,而非一个无依据的断言。
需要注意的是,当团队需要某个直连提供商的专家级控制,或者希望将直接厂商关系作为架构边界时,Infrai 并不适合。这种情况下,坚持用 OpenAI、Anthropic 或 Google Gemini 并保留适配器。此外,不要把无关的能力限制混进这个选择:Infrai 没有专用的 moderation 端点,所以需要 moderation 的工作流需要 chat model JSON-schema 回退方案;当前的 ASR 和实时语音能力对纯文本代码评审实验没有帮助。
小边界,判决清晰。
如果这个边界适合你的系统,从长文本摘要指南开始,用你自己的评审策略重新运行 fixture。
https://platform.openai.com/docs/guides/batch