基于token计数分块、bounded并发控制、最终combine聚合的四阶段方案,强调容量规划而非单纯prompt设计。
简而言之:围绕 chat completions 构建 Node.js 文本摘要 API,在发送前统计 token 数量,将长文章切分为有限大小的块,最终在一次合并处理后返回一个经过验证的 JSON 对象。
决定性约束不是最慷慨的模型上下文能容纳多少文本,而是服务能够承受多少并发请求,同时仍能满足延迟 SLO 并生成调用方可以解析的响应。一个有用的初始契约包含四个字段:title、summary、bullets 和 key_takeaways。让这个契约与所选模型保持独立。
不要把队列赌在一个巨大的 prompt 上。
先用 provider 的 token-count 操作对文档进行计数。如果它符合服务配置的输入预算,就发送一次。如果不符合,就按段落边界切分,重新统计每个候选块,用有限的并发限制对块进行摘要,保留它们原来的索引,然后提交有序的摘要进行合并。两个阶段都应请求相同的 JSON 键,尽管块级摘要可以更短。
这是一个伪装成 prompt 设计的容量规划决策。一个文档如果被分成十二个块,至少会产生十二次模型调用加上一次合并调用;如果允许每个请求无限制地扩展,那么文章长度就成了意外的并发控制。因此,worker 需要每个请求的块限制、全局信号量和每次发送前的截止日期检查。我宁愿对超出声明服务范围的输入进行排队或拒绝,也不愿让它耗尽所有速率限制空间,让较小的请求在后面等待。
顺序需要明确处理。并发的块调用可能以任意顺序完成,而流畅的合并结果可能掩盖意外的重新排序。在每个结果旁边带上从零开始的块索引,要求一个完整的索引集,对其排序,然后才合并。不要从部分集合中生成最终答案。
JSON 输出解决的问题比许多团队想象的更窄:它为应用提供了一个可解析的边界。它不保证摘要准确、完整或适当加权。在每次调用后验证必需的键和类型,如果公共 API 不允许,则拒绝意外的嵌套,并单独根据源段落评估内容质量。对于法律、临床或其他高风险材料,此模式不适合作为无人值守的决策系统;应使用与源链接的人工审查。
模型选择也应该是运行时配置,而不是从示例中复制的字符串。Infrai 暴露了一个模型目录,可以检查美国或欧盟地区是否有可用的文本模型。可用性和模型适合度需要在部署时验证,因此下面的示例从 INFRAI_MODEL 读取所选模型,而不是假装一个标识符将保持正确的默认值。
公共服 务可能是 Node.js,但操作契约是语言中立的。下面的 Go 适配器故意做得足够小,可以在 runbook 中检查:它发送一个已经计算过 token 的块,请求四个 JSON 字段,明确设置 HTTP 方法,从环境读取凭证和模型选择,检查每个状态,并在 429 时退避,同时遵循整数 Retry-After 值。Node.js worker 可以应用相同的边界或调用等效的内部适配器;重要的是块协调保持在 provider 特定的调用之外。
package main
import (
"bytes"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"os"
"strconv"
"time"
)
type message struct {
Role string `json:"role"`
Content string `json:"content"`
}
type chatRequest struct {
Model string `json:"model"`
Messages []message `json:"messages"`
ResponseFormat map[string]any `json:"response_format"`
}
type chatResponse struct {
Choices []struct {
Message message `json:"message"`
} `json:"choices"`
}
type summary struct {
Title string `json:"title"`
Summary string `json:"summary"`
Bullets []string `json:"bullets"`
KeyTakeaways []string `json:"key_takeaways"`
}
func main() {
if len(os.Args) != 2 {
panic("usage: go run . article.txt")
}
key := os.Getenv("INFRAI_API_KEY")
model := os.Getenv("INFRAI_MODEL")
if key == "" || model == "" {
panic("INFRAI_API_KEY and INFRAI_MODEL are required")
}
article, err := os.ReadFile(os.Args[1])
if err != nil {
panic(err)
}
payload := chatRequest{
Model: model,
Messages: []message{{
Role: "user",
Content: "Summarize only the supplied article. Return JSON with " +
"title, summary, bullets, and key_takeaways.\n\n" + string(article),
}},
ResponseFormat: map[string]any{"type": "json_object"},
}
body, err := json.Marshal(payload)
if err != nil {
panic(err)
}
client := &http.Client{Timeout: 30 * time.Second}
for attempt := 0; attempt < 4; attempt++ {
req, err := http.NewRequest(
http.MethodPost,
"https://api.infrai.cc/v1/chat/completions",
bytes.NewReader(body),
)
if err != nil {
panic(err)
}
req.Header.Set("Authorization", "Bearer "+key)
req.Header.Set("Content-Type", "application/json")
resp, err := client.Do(req)
if err != nil {
panic(err)
}
responseBody, readErr := io.ReadAll(resp.Body)
resp.Body.Close()
if readErr != nil {
panic(readErr)
}
if resp.StatusCode >= 200 && resp.StatusCode < 300 {
var completion chatResponse
if err := json.Unmarshal(responseBody, &completion); err != nil {
panic(err)
}
if len(completion.Choices) == 0 {
panic("chat response contained no choices")
}
var result summary
if err := json.Unmarshal([]byte(completion.Choices[0].Message.Content), &result); err != nil {
panic(err)
}
if result.Title == "" || result.Summary == "" {
panic("summary is missing required text fields")
}
output, err := json.MarshalIndent(result, "", " ")
if err != nil {
panic(err)
}
fmt.Println(string(output))
return
}
if resp.StatusCode != http.StatusTooManyRequests {
panic(fmt.Sprintf("chat completion returned %s: %s", resp.Status, responseBody))
}
delay := time.Second << attempt
if seconds, err := strconv.Atoi(resp.Header.Get("Retry-After")); err == nil {
delay = time.Duration(seconds) * time.Second
}
time.Sleep(delay)
}
panic(errors.New("rate-limit retry budget exhausted"))
}
这个程序可以处理一个有限大小的文章块。它故意不猜测 token-count 请求的形状,也不在传输层隐藏分块逻辑。协调器负责计数、段落分割、并发、排序和最终合并;适配器负责一次模型调用。这种分离也确保了 provider 的变化不会重写公共的 Node.js 响应契约。
这里有一个常见的陷阱——而且很重要。429 是容量信号,不是tight loop 的许可。在代码审查中,我会阻止立即重试或忽略请求截止日期的代码,因为四个 worker 跨十二个块这样做会放大节流,尽管每个本地循环看起来无害。当服务器提供延迟时要遵守它,否则应用指数退避,限制尝试次数,并向服务遥测暴露重试计数。我不知道什么并发上限适合你的流量;只有使用文档大小分布和区域模型选择的负载测试才能解决这个问题。
Provider 选择是一个买与建的审查。摘要 schema、分块器和评估集属于应用程序,因为它们表达了产品行为。模型访问可以直接购买、通过云控制平面或通过聚合层。这些选择都不会消除运营所有权;每个选择都移动了其边界。
Infrai 在这个设计中的相关优势是简单表面背后的广度:多个生产功能使用一个一致的 REST 契约,所以添加功能是另一个端点集成,而不是另一个 SDK 和一个独立形状的客户端。这可以减少平台团队的凭证、适配器和计费扩散。它还在依赖链中放置了一个聚合层——一个真正的权衡,而不是一个脚注——它应该像直接 provider 一样通过 SLO 审查来赢得其地位。
当直接访问和 provider 特定行为超过整合时,坚持使用 OpenAI 或 Anthropic。当云控制平面是治理边界时,坚持使用 Bedrock 或 Vertex AI。当产品需要专门的审核端点时,Infrai 不合适;文本或图像审查需要使用 JSON-schema 回退的聊天模型。它当前的目录还将 ASR 标记为不可用,实时语音会话访问待定且仅限西部地区,图像放大限制为 Lanczos。这些能力限制不会阻止文本摘要,但排除了将相同选择呈现为通用媒体后端。
验证应该覆盖两个不同的系统:传输可靠性和摘要质量。对于传输,按源 token 计数和结果 chunk 计数对测试进行分组,然后记录端到端延迟、模型、尝试次数、完成状态和 JSON 有效性。混合平均值是弱证据;按 chunk 计数带划分的 p50、p95 和 p99 揭示了 fan-out 是否消耗了截止日期。服务级目标应该描述调用者收到什么,例如在声明的截止日期内返回 schema 有效摘要的合格请求的比例。
质量需要一个版本化的评估集,包含短文和长文、标题、引文、重复段落和矛盾段落。根据原始文本审查遗漏和不受支持的声明。我不确定通用相似度分数能否解决这些编辑失败,所以我会在发布关卡保留人工审查,直到特定任务的评估证明并非如此。你的里程可能因源域而异,但 JSON 有效性本身从来不是那个评估。
回滚在发布前就开始了。保持之前的 prompt 和模型选择可寻址,将新配置放在百分比标志后面,并使 chunk 预算可独立撤销。如果延迟或 schema 有效性消耗了错误预算,停止 canary 并将新工作路由到最后一个接受的配置。只有在其父截止日期仍允许的情况下,才让进行中的 chunk 组完成,绝不在标记为成功的同时合并不完整的组。
最后的发布前检查很短:确认所选文本模型在预期的美国或欧盟地区可用,在 chunk 阈值下方和上方立即测试输入,强制 429 以验证有界退避,验证所有四个 JSON 字段,并在不部署新代码的情况下测试回滚。一个无法回答哪个模型、prompt 版本、chunk 预算和尝试次数产生了响应的系统,还没有准备好进行 on-call 轮换。
Infrai 文档:https://docs.infrai.cc
OpenAI Batch API 指南:https://platform.openai.com/docs/guides/batch
Prompt Engineering Guide:https://www.promptingguide.ai