Jean's Blog

一个专注软件测试开发技术的个人博客

0%

Agent基础:生成参数、Token与上下文管理

本文是 LLM API 基础系列第二篇。上一篇:LLM API 模型调用服务;下一篇:结构化输出与工具调用。

厂商参数支持核对于 2026-09-21。本文区分一般采样原理与具体服务行为,不把某家实现当成通用协议。

1. 三件容易混淆的事

要控制什么 常见参数或机制 不能误解为
生成的随机性 temperature、top_p 事实正确性的保证
单次生成的预算 max_tokens、max_completion_tokens、max_output_tokens 必须生成到这个长度
请求能容纳多少信息 Context Window、输入裁剪、摘要、检索 永久保存全部聊天历史

调参会影响任务表现,但不能把参数当成万能质量旋钮。回答不符合要求时,先检查任务是否明确、证据是否充分、模型是否具备所需能力,再设计对照实验。

2. temperature 与 top_p:原理不等于接口承诺

2.1 temperature 控制采样分布的集中程度

在常见采样实现中,温度作用于 token 候选的分数:低温更偏向高概率候选,高温让分布更平缓。它改变的是生成策略,不是给模型增加知识。[1]

  • 需要一致性时,可以从服务默认值或较低温度开始评测。
  • 需要探索时,可以比较更高温度下的多样性与有效率。
  • temperature=0 不等于 API 承诺逐字复现。服务版本、执行环境等因素仍可能影响结果;不能只凭一次差异就确定原因。[2]

0.2 可以是某次实验的候选值,不是“所有 Agent 的最佳默认值”。高温也不意味着无法输出合法 JSON,格式保证要看结构化输出机制,而不是只看温度。

2.2 top_p 限制候选概率质量

核采样通常按概率从高到低保留累计概率达到阈值的候选集合,再从中采样。top_p=1 表示不通过这个阈值缩减候选集,不表示均匀随机抽取整个词表。[1]

为了方便解释实验结果,一般一次主要调整一个采样参数。不是说同时设置两者一定错误,更不是说同时调整两者才会导致“不确定性”。

2.3 先确认参数是否真的生效

这是原文采样实验最需要修正的地方。 DeepSeek 在核对日的文档中明确规定:[3]

DeepSeek 模式 temperature top_p
非思考模式 可配置 固定为 1.0,传入值被忽略
思考模式 不生效 有效范围为 0.95–1.0,低于 0.95 按 0.95 处理

原脚本关闭 thinking 后扫描 top_p=1.0/0.7/0.3/0.1,按当前接口约定并没有改变有效的 top_p。不同组出现不同答案,不能据此归因于 top_p。

原运行记录没有固定的底层模型版本和足够元数据,不能仅凭当前文档反推当时服务的所有行为;但它不应继续作为当前接口上的有效参数对照实验。

OpenAI 推理模型的参数支持也随模型变化。不要套用“所有推理模型都会忽略参数且不报错”的规则;有的组合会直接被拒绝。[4]

3. 如何做一个不误导自己的小实验

本节保留原文“给函数起名”的任务,只观察当前 DeepSeek 非思考模式下的温度差异。不是模型质量排行榜。

沿用第一篇 .env。以下脚本可独立运行,默认调用 15 次付费 API;先确认账户预算。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
import os
from collections import Counter

from dotenv import load_dotenv
from openai import OpenAI

load_dotenv()
client = OpenAI(
api_key=os.environ["DEEPSEEK_API_KEY"],
base_url=os.getenv("DEEPSEEK_BASE_URL", "https://api.deepseek.com"),
timeout=30.0,
max_retries=0,
)

TRIALS = 5 # 只做冒烟观察,不足以形成统计结论
PROMPT = "为一个统一归一化模型错误的 Python 函数起名,只输出一个 snake_case 名称。"

for temperature in (0.0, 0.5, 1.0):
values = []
for _ in range(TRIALS):
response = client.chat.completions.create(
model=os.environ["DEEPSEEK_MODEL_NAME"],
messages=[{"role": "user", "content": PROMPT}],
temperature=temperature,
max_tokens=128,
extra_body={"thinking": {"type": "disabled"}},
)
choice = response.choices[0]
if choice.finish_reason != "stop" or not choice.message.content:
raise RuntimeError(f"样本无效:{choice.finish_reason}")
values.append(choice.message.content.strip())
print("model:", response.model, "usage:", response.usage)
print("temperature:", temperature, "counts:", Counter(values))

观察结果时,把事实、假设和结论分开:

能直接观察到的事实 不能直接推出的结论
5 次采样里出现 2 个不同名称 候选答案总共只有 2 个
几组的去重数刚好相同 参数无效,或存在明确的温度“阈值”
温度为 0 时仍有不同回答 一定是 MoE 路由或浮点误差造成的
某组答案种类更多 该组更准确、更适合生产

如果要做可信评测,应扩大样本、覆盖多个任务、重复运行,并记录模型标识、日期、提示词、实际参数、失败率和成本。具体样本量由目标差异和容许误差决定,30 次或 50 次也不是可信结论的自动通行证。

多样性指标可以辅助分析,但结构化任务还要看 schema 通过率和任务成功率。原文长篇运行日志及对后端机制的猜测不再作为教程结论保留。

4. 输出预算:上限不是实际花费

4.1 参数名称因端点而异

端点 常用预算字段 注意
DeepSeek Chat Completions max_tokens 按 DeepSeek 的输出限制配置
OpenAI Chat Completions max_completion_tokens 当前文档推荐字段;旧 max_tokens 不适用于所有模型
Responses max_output_tokens 推理模型可能把推理 token 计入预算

这些字段都是上限,不是“要求模型必须写够”。计费通常依据实际处理的输入与输出 token,以及缓存等计费规则;提高上限不必然增加实际费用,也不会本身导致更容易截断。[1][4][5]

更准确的关系是:

  • 在其他条件相同且请求被接受时,提高输出上限通常降低因该上限而截断的风险。
  • 更大的上限允许产生更长输出,因此可能增加最坏情况下的成本和等待时间。
  • 预算过小,推理模型甚至可能在产生可见答案前就用完额度。
  • 给模型更大输出预算,不会自动扩大上下文窗口,也不会保证答案正确。

4.2 截断要与正常结束分开

接口 应检查的信号 处理方式
Chat Completions finish_reason == "length" 检查输出额度及上下文限制
Responses status == "incomplete" 和 incomplete_details.reason 按具体原因处理,例如 max_output_tokens

不要一看到 incomplete 就假定都是预算不足;也不要只靠文本“看起来完整”判断成功。[1][4]

对于文章生成,可向用户展示明确标记为不完整的结果;对于工具参数、JSON 或代码修改,不要执行截断结果。应调整预算、缩小任务或重新生成完整结构。

5. Token 与 Context Window

Token 是模型处理文本或其他输入时使用的单位,不等于一个汉字、一个英文单词或一个字符。不同模型和输入类型的计数方式可能不同。[5]

做容量规划时,可以先使用下面这个教学模型:

1
2
3
4
输入估算 + 计划输出预算 + 安全余量 ≤ 上下文容量

输入估算包含:
应用指令 + 当前问题 + 保留的历史 + 工具定义 + 工具结果 + 消息封装开销

还要单独满足模型的最大输入/最大输出限制;推理 token、多模态和缓存的具体核算依服务而定。缓存可能降低成本,但不等于这部分信息不占上下文。[4][5]

graph TD
    A[整理输入并预留输出预算] --> B{容量是否足够}
    B -->|是| C[发起请求]
    B -->|否| D[筛选材料或压缩历史]
    D --> E{关键内容能否保留}
    E -->|能| A
    E -->|仍放不下| F[拆分任务或报告超预算]

“历史太长会挤掉系统指令”不是所有 API 的自动行为。超窗后可能报错;只有应用或服务启用了某种截断策略时,内容才会被移除。例如 DeepSeek Responses 不支持 truncation,超窗会返回错误。[6]

5.1 计数是工程估算,不要承诺绝对精确

  • 使用目标模型适用的 tokenizer 或厂商计数接口。
  • 文本编码结果之外,还要考虑消息封装、工具定义等开销。
  • 不要把 len(text) 当 token 数,也不要把 OpenAI 的 tokenizer 当所有模型的通用精确计数器。
  • 用实际响应的 usage 校准估算,同时保留安全余量。[7]

服务返回的 usage 用来统计实际用量;请求前的计数用来规划预算。二者解决的是不同问题。

5.2 裁剪应保留完整的工具交互

下面只演示预算选择算法,不声称实现某个模型的精确计数。调用者必须提供适配目标端点的 estimate_tokens,其输入是完整消息列表。

历史按完整轮次组织:一轮包含用户请求、相关 assistant 输出,以及全部配对的工具调用与结果。先在应用中完成分组,不能直接把任意单条消息当成一轮。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
from collections.abc import Callable


def build_messages(
history_turns: list[list[dict]],
task: str,
*,
instructions: str,
context_limit: int,
output_budget: int,
safety_margin: int,
estimate_tokens: Callable[[list[dict]], int],
) -> list[dict]:
if min(context_limit, output_budget) <= 0 or safety_margin < 0:
raise ValueError("预算参数非法")

input_budget = context_limit - output_budget - safety_margin
fixed = [{"role": "system", "content": instructions}]
current = [{"role": "user", "content": task}]
if estimate_tokens(fixed + current) > input_budget:
raise ValueError("仅固定指令与当前任务就超出输入预算")

recent = []
for turn in reversed(history_turns):
candidate = list(turn) + recent
if estimate_tokens(fixed + candidate + current) > input_budget:
break # 只保留连续的最近完整轮次
recent = candidate
return fixed + recent + current

estimate_tokens 应把本次固定工具定义等额外开销也计入估算。这段代码不处理多模态,也不验证原历史的协议合法性。

它修正了按单条消息裁剪的风险:不能只留下 tool 结果,却删除对应的 assistant.tool_calls。对尚未完成的工具交互,不应当作可随意丢弃的历史轮次。[8]

长期运行时,可以结合摘要和检索,但摘要本身也可能丢信息。关键任务约束和工具证据应保留可追溯来源,不能靠“模型应该记得”维持状态。

6. 本篇小结

  • 先检查服务是否支持参数,再做实验;传入成功不等于生效。
  • 输出上限、实际输出、上下文容量是三个不同概念。
  • 低温不保证完全确定;少量样本不足以解释模型后端机制。
  • 裁剪历史时,保留任务约束和完整工具交互,不能只按字符数删消息。

下一篇:结构化输出与工具调用。

参考资料

  1. DeepSeek:Chat Completions API
  2. OpenAI:Chat Completions API — seed 与确定性说明
  3. DeepSeek:Thinking Mode
  4. OpenAI:Reasoning models
  5. DeepSeek:Models & Pricing
  6. DeepSeek:Using the Responses API
  7. OpenAI Cookbook:How to count tokens with tiktoken
  8. OpenAI:Function calling