一次 LLM Call 如何被应用管理:从 Request 契约到可观测 Response
一次 LLM Call 如何被应用管理:从 Request 契约到可观测 Response
第一篇说明了一次 LLM call 的本质:模型基于当前 Context 完成一次推理,返回一个 Response。这个结论还不够让应用稳定工作,因为应用仍要回答三个工程问题:这次到底提交了什么?模型依据了什么?收到结果后该如何处理与排错?
本文讨论的不是怎样让模型“更聪明”,而是怎样把一次推理变成一份可组装、可观察、可消费的调用契约。
候选 Input
↓ 应用选择与组装
Request ──> 模型服务准备 Context ──> Inference ──> Response / Events
↑ ↑ ↓
可完整记录 只能部分观察 应用校验、记录与消费
| 概念 | 本文的工作定义 | 谁主要负责 |
|---|---|---|
| Input | 可能供本次调用使用的信息 | 用户、应用、工具或存储系统 |
| Request | 应用发给模型服务的调用契约 | 应用或 Runtime |
| Context | 模型在本次推理中可见的信息 | 服务与模型,应用只能部分控制 |
| Inference | 模型据 Context 生成结果的计算过程 | 模型服务 |
| Response | 服务返回的结果、状态和相关元数据 | 模型服务;应用负责消费 |
Request 不是 Prompt,而是一份调用契约
用户输入“调研三个主流 Agent Runtime,并给出选型建议”,只是一个 Input。它还没有规定模型采用什么边界、哪些历史资料可用、下游程序期望何种格式,因而不是一份完整 Request。
应用需要把这些决定明确写入请求。不同供应商的字段名称不同,但一份调用契约通常包含以下部分:
| 契约部分 | 解决的问题 | 常见来源 |
|---|---|---|
| model | 由哪个模型执行本次推理 | 产品配置或路由策略 |
| instructions | 哪些规则在本次调用中有效 | 开发者或产品 |
| input / messages | 模型应回应什么、参考哪些历史 | 用户与会话策略 |
| tools | 模型可以提出哪些外部能力请求 | Tool Registry |
| output schema | 下游代码需要怎样的结果结构 | 消费方的接口契约 |
| generation config | 长度、采样或推理预算如何控制 | 应用配置 |
| metadata | 如何把这次调用关联到业务日志 | Runtime |
以 C02 为例,应用可以构造如下 Request:
{
"model": "<configured-model>",
"instructions": "你是技术研究助手。本次没有搜索、文件或记忆工具。不得声称已核验实时资料;信息不足时明确说明。",
"input": [
{
"role": "user",
"content": "比较三个主流 Agent Runtime,并列出形成选型建议前仍需确认的信息。"
}
],
"tools": [],
"output_schema": "research_summary",
"max_output_tokens": 800,
"metadata": { "case": "C02", "run_id": "run_..." }
}
这里的重点不是字段拼写,而是每个字段都有可追溯的来源与职责。Prompt 或 messages 只是契约的一部分;图片、文件片段和 Schema 同样会改变本次调用。把它们只当成“提示词”来管理,出现问题时就无法知道哪条规则、哪段历史或哪个格式约束造成了结果。
Input、Request 与 Context 不是同一个东西
这三个词常被混用,却处在不同阶段:
所有可用资料 ──筛选、截断、摘要──> Request ──服务处理──> Context
Input 是候选集合。比如数据库有一百篇资料、会话里有二十轮历史、工具刚返回十个网页片段;应用可能只选择其中三段写入本次 Request。未被选择的信息,对这次推理没有作用。
Request 是应用能完整保存和复现的边界。它说明应用试图让模型看到什么。Context 则是模型在这次推理中实际可见的信息:服务可能执行消息模板、Tokenization 或内部处理后才形成它。应用通常不能逐 Token 查看这一内部序列,因此不应把聊天界面显示的内容误当为完整 Context。
这一区分带来一个实用的排错顺序:模型似乎遗漏了一项要求时,先检查该要求是否在保存的 Request 中;再检查截断、摘要和上下文窗口策略;最后才判断模型是否没有遵循它。资料“存在于数据库”不是证据,资料“出现在本次 Request 中”才是。
Response 是调用结果,也是应用的消费接口
模型服务完成 Inference 后返回 Response。对应用而言,Response 不应只被当成一段要展示的文本,而应被当成一次调用的结果记录:它可能包含结果内容、模型标识、调用 ID、完成状态、用量、错误,或结构化的工具调用意图。
一个适合下游消费的结果可以抽象为:
{
"id": "resp_...",
"status": "completed",
"model": "<actual-model>",
"parsed": {
"research_question": "比较三个 Agent Runtime 并明确选型前提",
"comparison_dimensions": ["状态管理", "人工审批", "可观测性"],
"known_limitations": ["未检索实时官方资料", "尚未确定业务约束"]
},
"usage": { "input_tokens": 112, "output_tokens": 489 }
}
其中 parsed 适合交给后续代码,原始 Response 则应保存在 artifact 或日志中,供问题复盘。二者不能互相替代:只存展示文本,缺少状态和用量;只存解析结果,又丢失服务返回的原始证据。
Schema 解决结构问题,不解决事实问题
如果下游需要稳定字段,可以声明一个输出 Schema:
const ResearchSummary = z.object({
research_question: z.string(),
comparison_dimensions: z.array(z.string()),
known_limitations: z.array(z.string()),
});
这让程序不必从自由文本中猜测字段位置,也能及时发现结构无法消费的结果。但 Schema 只证明“数据长得像约定的样子”。即使 JSON 校验成功,其中关于产品能力或版本的事实仍可能过时、缺乏来源或与用户需求无关。事实核验属于后续的任务流程,不能被格式校验冒充。
同样,Response 中出现 Tool Call 只代表模型返回了一个结构化意图,例如“希望调用 search_docs 并附上参数”。它不是工具已经执行的证明。工具执行、权限检查和结果回填将在第四篇讨论。
Streaming 改变传输方式,不增加推理步骤
非流式调用在生成结束后一次返回完整 Response:
Request ─────────等待─────────> 完整 Response
流式调用则把同一次调用的结果拆为事件,让应用可以提早显示或处理增量内容:
Request ─> response.created
─> output_text.delta
─> ...
─> response.completed
逐字出现的界面效果不等于模型被调用了很多次,也不等于产生了多个 Agent Step。它只是同一 Response 的传输方式不同。
应用在流式场景中需要多做两件事:按顺序聚合事件,并以完成事件或最终状态作为“结果完整”的依据。连接中断时,已有的 delta 只能视为部分传输;SDK 重试时,也要防止把重复事件拼进同一份结果。把每个关键事件、Response ID 和最终状态记录下来,才能区分“模型没有完成”与“模型完成了但客户端没有收全”。
C02:把一次调用变成可复盘记录
C02 不搜索、不执行工具,也不运行 Agent Loop。它的交付物不是一段漂亮回答,而是一份能够回答“这次调用究竟发生了什么”的证据:
- 保存完整 Request,并标注每个字段的来源。
- 以相同业务意图分别进行非流式与流式调用;保存完整 Response 或完整事件序列。
- 对结构化输出执行
research_summary校验,同时保留原始 Response。 - 为每次调用记录
run_id、Response ID、模型、状态、耗时、Token usage 与错误原因。
两种调用的文字不必逐字相同;采样和服务执行会带来差异。C02 要验证的不是文案一致,而是应用能够准确说明自己发送了什么、收到了什么,以及结果是否完整可消费。
| 失败 | 可观察现象 | 优先检查的位置 |
|---|---|---|
| Context 超限或被裁剪 | 服务拒绝 Request,或关键要求没有进入结果 | 输入选择、摘要与 Context 策略 |
| 流连接中断 | 只收到部分事件,没有完成状态 | 传输、重连与事件聚合 |
| Schema 校验失败 | 有 Response,但下游字段不可用 | 输出契约与校验逻辑 |
| 事实不可靠 | 结构正确,却无法给出来源或与最新资料冲突 | 任务的取证与验收流程 |
代码已上传至 GitHub 仓库 AgentLoopExperienceSample。
小结
一次 LLM call 的推理发生在模型服务中;应用真正能设计和审计的是调用两端:发出的 Request 与收到的 Response / Events。Request 是一份明确的调用契约,Context 是它经服务处理后形成的模型可见范围,Response 则需要被校验、记录并交给下游消费。
下一篇转向更大的边界:用户交给系统的是一个目标,为什么它通常不能直接等同于一份 Request,更不能由一次成功的 Response 宣告完成。
案例实践清单
- 为 C02 保存 Request,并标注每个字段的来源与用途。
- 使用相同业务意图分别执行非流式和流式调用。
- 保存流式事件、聚合结果与最终完成状态。
- 使用
research_summarySchema 校验结构,同时保存原始 Response。 - 记录 run ID、Response ID、模型、状态、Token usage、耗时与失败原因。
参考资料
- OpenAI API:Streaming responses:SSE 流式传输和响应事件。
- OpenAI API:Structured model outputs:使用 JSON Schema 约束模型输出。
- OpenAI API:Conversation state:上下文续接方式。
- Anthropic API:Messages:另一种 Messages API 的 Request/Response 设计。
- Hugging Face Transformers:Chat templates:消息与模型输入序列之间的转换。