LLM 审阅接口如何兼容批处理与流式输出
文章建议统一大文档与代码审阅的请求、结果结构,再按交互需求选择批处理或流式传输。分块、校验、重试和供应商适配独立处理,严格结构化报告需避免直接使用未完成的流式文本。
AI 深度解读
大文档接入 LLM API,应该一次跑完,还是边生成边展示?作者给出的判断是:先统一审查请求和结果,再选择批处理或流式传输。定时审查一批 PR,重点是完成、留档和重跑;开发者等着检查一个模块,重点是尽早拿到有效发现。
这篇的价值在于把选型问题拆开:任务怎么执行、进度怎么展示、结果怎么认定,分别设计。它没有给出性能对照实验,但对准备把 AI 代码审查做成长期服务的团队,有一套可以直接借鉴的接口边界。
先统一审查结果,切换模型才不用重写业务
给某家 SDK 套一层 summarize,并不等于实现了供应商无关。
如果业务代码仍然依赖供应商特有的消息角色、结束原因、token 字段和重试方式,更换模型时,包装函数之外的代码照样要改。作者建议先定义内部契约:请求表达审查什么,结果表达发现什么,供应商的请求 ID 则放进追踪元数据。
批处理和流式可以有不同的执行过程,但必须交付同一种最终结果。
下面这段接口把完整执行和事件输出放在同一个边界内,解决两种模式各自长出一套数据结构的问题:
type Severity = "info" | "warning" | "error";
type ReviewRequest = {
schemaVersion: "1.0";
documentId: string;
chunks: Array<{ id: string; text: string }>;
idempotencyKey: string;
};
type Finding = {
severity: Severity;
message: string;
chunkId: string;
};
type ReviewResult = {
status: "complete" | "failed";
findings: Finding[];
};
type ReviewEvent =
| { type: "progress"; completedChunks: number; totalChunks: number }
| { type: "finding"; finding: Finding }
| { type: "complete"; result: ReviewResult }
| { type: "error"; message: string };
interface ReviewRuntime {
run(request: ReviewRequest): Promise<ReviewResult>;
stream(request: ReviewRequest): AsyncIterable<ReviewEvent>;
}
重点看最后两行:run 和 stream 接受相同的 ReviewRequest,流式通过 complete 事件交付同一种 ReviewResult。progress 和 finding 增加的是过程信息。
schemaVersion: "1.0" 和稳定的 chunk ID,会增加少量维护工作,却能让历史报告在适配器变化后仍然可解释。测试也有了明确对象:可以用假的 runtime 返回两条发现,测试业务处理;再单独验证适配器是否拒绝格式错误的模型输出。
不过,示例只是最小骨架。作者文字中要求的预期输出 schema、来源区间和部分元数据,并没有完整体现在这些类型里。chunkId 能定位到片段,还不能精确定位到代码行。
流式连接断了,审查任务怎么办
作者把两种模式的区别概括为:批处理改变调度,流式改变观察方式,审查规则不该跟着变化。
批处理提交前,应保存规范化请求;幂等键(idempotency key)应来自文档修订版本,不能来自一次性的队列消息。幂等键用来识别同一次业务操作,避免重试或重连反复创建任务。最终结构化结果需要保存,原始响应则在政策允许时单独保存,方便追查和切换适配器后重跑。
流式的麻烦在于:连接状态和任务状态不是一回事。
假设教师已经收到两条有效发现,随后浏览器断网。服务端可能还在运行,也可能已经完成。客户端应该能根据文档修订版本重新连接并查询权威结果,而不是因为没收到最后一个事件,就重新提交整份审查。
这需要持久化 queued、running、complete、failed 等状态,并明确断连后继续、取消还是恢复。代码里的 ReviewResult.status 只有终态,任务执行状态还需要另行设计。
需要保证完成的工作依赖持久状态,实时连接负责通知。
前端也不该通过拼接模型文本来还原最终 JSON。文字可以用于展示,业务数据应通过结构化事件传递。每个事件都应能独立处理。
如果部分发现不能直接作为正式结论,可以采用缓冲流式:UI 展示进度,服务端收集发现,等完整对象通过校验后再发布。代价是额外内存和等待时间,适合只能留下一份正式报告的评分或审查流程。
大文档拆块,不能只盯着长度
即使请求接口统一了,切片出错仍然会破坏审查质量。把修改后的函数与 imports 分开,或者把评分规则与被评估代码分开,模型可能拿不到判断所需的上下文。
作者要求切片保留稳定 ID、文档顺序和足够上下文,并提出一个起始策略:目标长度 12,000 字符,重叠 1,000 字符。这里按字符计数,不是 token;原文也明确说,这组数值不是基准测试结论。
具体长度仍取决于文档格式、模型上下文限制和代码密度。切片策略还应记录进请求元数据,否则以后很难解释同一文档为什么得到不同报告。
每条发现都应绑定 chunkId。缺少证据位置的结论,需要标记为人工复核或按 schema 拒绝。对于代码审查,措辞再流畅,也不能替代可回查的依据。
输出校验同样不能只看有没有 findings 数组。作者建议进一步检查每个 severity、确认 chunk ID 存在、限制消息长度,并在重复证据会误导读者时进行去重。JSON Schema 或同类类型校验器,更适合作为统一边界。
原文第二段实现还留有一个需要补上的缺口:reviewLargeChange 在没有收到 complete 事件时,会把累计发现包装成 status: "complete" 返回。如果迭代因为意外而提前结束,这个回退可能把部分结果认成完整报告。生产实现需要明确完成信号和异常结束的区别。
至于选哪种模式,作者给了四个判断问题:断连后是否允许继续、是否必须重跑复现、部分发现能否安全地指导行动、是否需要更换供应商而保留历史报告。
度量也要对应这些问题:首条有效发现耗时、最终校验结果耗时、断连后完成情况、重试次数、无效输出率,以及发现是否带有可用证据区间。按文档大小和审查模式分别统计,才能看到平均值掩盖的问题。
我的判断
我认同把契约放在传输方式前面。对需要长期保存审查记录的系统,统一结果比统一 SDK 调用形式更重要。队列承载定时任务、事件承载交互进度,两条路径共享切片、校验和供应商适配接口,这个边界也容易理解。
但原文是一份架构建议,没有展示真实团队的对照数据。它没有证明流式能节省多少等待时间,也没有证明 12,000/1,000 字符的切法优于按函数或模块切分。相关判断应当作为待验证的起点。
适用前提也要看清。教育评分需要正式报告和可追溯记录,为此增加任务状态和持久化是合理投入;一次性本地审查脚本未必需要同等复杂度。先用 run 完整执行、校验后返回,可能已经够用。
统一 schema 还只能解决格式兼容,不能保证模型结论一致。同一份请求交给另一家模型,发现数量、严重程度和证据质量仍可能变化。能重跑代表过程可追查,不代表结果可复现到完全相同。
最后,标题谈大文档总结,正文实际围绕教育代码审查展开。带严重程度和证据位置的 Finding 很适合审查任务;如果做的是普通文档摘要,结果结构仍需按产品需求重新定义。
能拿来做什么
- 做前端或 Node 的 AI 审查功能时,先定义
ReviewRequest、Finding和ReviewResult,让页面依赖内部事件与结果,再通过适配器接入供应商。 - 给交互式审查增加流式展示时,同时实现任务状态查询和稳定幂等键,让浏览器断线后能找回最终报告。
- 从 12,000 字符、重叠 1,000 字符开始试验切片,但要检查函数、imports 和审查规则是否被拆散,并要求每条发现关联有效
chunkId。 - 上线前分别记录首条有效发现和最终校验结果的耗时;如果部分发现不能安全使用,就先展示进度,完整校验后再展示正式结论。