Appearance
一次模型调用受哪些硬边界约束:Token、Context Window、Sampling 与 Latency
前置:01 Foundations:模型建议权与 Runtime 事实提交权
先看一个真正会把 Harness 卡死的问题
假设某模型标称支持 128K context。Harness 当前有 70K 历史、20K 检索结果、15K Tool schema,于是程序判断“总共 105K,还没到 128K,可以发”。请求却可能仍然失败,或者成功后没有足够空间生成答案。
原因是 context window 不是 history 的容量,而是一次 inference 的总工作空间。输入消息、工具定义、检索证据、模型输出,以及某些 Provider 暴露或内部使用的额外 token,都要争夺这块空间。Harness 因此不能把 Provider 上限直接当成 max_input_tokens。
这也是为什么 Model Layer 必须先给 Runtime 一个稳定的预算模型,而不是让 ContextManager 自己猜。
Token 和 Context Window 分别约束什么
Token 是模型实际处理文本/结构化输入时使用的离散单位。字符数、字数和 token 数没有固定一一对应关系,因此生产系统应使用目标模型对应的 tokenizer 或 Provider usage 数据估算,而不是用 len(text) 当真实预算。
Context Window 是单次模型调用能够共同容纳的全部 token 上限。工程上更有用的表达不是:
text
input <= context_window而是:
text
assembled_input
+ reserved_output
+ safety_margin
<= effective_context_limit例如:
python
@dataclass(frozen=True)
class ModelBudget:
context_limit: int
reserve_output: int
safety_margin: int
@property
def max_input(self) -> int:
return self.context_limit - self.reserve_output - self.safety_marginContextManager 消费的是 max_input,而不是 Provider 宣传页上的最大窗口。这样当模型、输出上限或策略变化时,预算仍然由 Model Adapter 统一解释。
Context Window 大,不等于 Context 越多越好
容量问题和信息质量问题要分开。
即使 100K 输入合法,也可能因为低相关历史、巨大 Tool Result 或几十个无关 Tool schema 把关键约束淹没。“装得下”只证明请求合法,不证明模型容易找到正确证据。 Context selection、progressive disclosure 和 compaction 的 canonical 机制在 Module 04;本文只确定它们必须服从的硬容量边界。
因此 Harness 至少应该记录:
text
input_tokens
reserved_output_tokens
tool_schema_tokens
retrieval_tokens
history_tokens
model_context_limit这样 context overflow 或成本异常才能定位到具体来源,而不是只看到“模型请求失败”。
Sampling 是概率控制,不是业务正确性开关
Temperature、top-p 等 sampling 参数改变的是下一 token 的采样分布。降低随机性通常有利于格式稳定和重复实验,但不能把概率模型变成确定性函数;即使相同输入和相同参数,Provider 实现、模型版本或服务端变化也可能让输出不同。
这给 Harness 两个直接约束:
- Runtime 的状态迁移、权限、重试次数、终止条件不能依赖“模型这次应该和上次说一样”;
- 想测试确定性软件行为时,用 Scripted/Fake Model,而不是强行把 temperature 调到某个值后假设输出必然相同。
Sampling 参数应该进入 Run Manifest,因为它是解释某次行为差异的重要运行条件,但它不是 execution truth。
Latency 要拆开看,否则不知道该优化哪一层
用户看到的“模型很慢”可能来自完全不同的位置:
text
request queued
→ network / provider admission
→ time to first token (TTFT)
→ token generation
→ stream transfer
→ client consumption对于 Agent Harness,还要把 Model latency 与 Tool / DB / Queue latency 分开。一个 Run 30 秒,不意味着模型用了 30 秒。
Model Adapter 至少应向 Trace 暴露:
text
request_started_at
first_token_at
request_finished_at
input_tokens
output_tokens
provider/model于是可以得到:
text
TTFT = first_token_at - request_started_at
generation_time = request_finished_at - first_token_atTTFT 高,可能是排队、网络或大输入预处理;generation time 高,可能是输出太长或模型本身生成慢。两种问题的优化方向完全不同。
这些边界怎样进入 Harness
Model Layer 不应该只返回一段文本。至少需要把“能力”和“本次调用事实”分开:
python
@dataclass(frozen=True)
class ModelCapabilities:
context_limit: int
max_output_tokens: int | None
supports_tools: bool
supports_streaming: bool
@dataclass(frozen=True)
class ModelUsage:
input_tokens: int
output_tokens: int
ttft_ms: int | None
total_ms: intCapability 用来在请求前做预算和功能选择;Usage 用来在请求后记账、Trace 和 Eval。不要拿上一次 Usage 推断当前模型的硬上限,也不要把 Provider SDK 的具体 response object 直接传进 Runtime。
一个最常见的失败链
错误设计:
text
context_limit = 128K
→ ContextManager 塞入 126K
→ Tool schema 临时增加
→ 还要求模型输出长 JSON
→ 请求在生产流量中偶发 overflow真正的修复不是“失败后截断最后一条消息”,而是从组装前就建立预算:预留输出和安全余量,给 history/retrieval/tool definitions 分配子预算,并把真实 token usage 反馈到观测系统。
这个模型会在后面的 Context、Tool、Performance 模块继续被使用,但不再重新解释 Token / Context Window 的基础语义。
自测
- 一次调用同时出现高 TTFT 和总耗时过长,你会怎样区分 queue wait、prompt processing、generation 与 tool wait,而不是笼统归因于“模型慢”?
- Context 已接近窗口上限时,为什么不能只把
max_output_tokens继续调大?输入预算和输出预留应如何共同约束一次请求?