
接触过 LLM 应用开发的人大概率遇到过这样的现象同一个 Prompt在本地调试时回答干脆准确换到测试环境或上线后却变得啰嗦、跑题甚至把 Agent 循环跑到超时。问题通常不是模型突然变笨而是应用层一直在使用模型厂商的默认配置。LLM Counter-Defaults 这个概念用于描述一组针对默认行为进行反向设计和计数器约束的方法把采样参数、提示词、循环次数、Token 额度都从“平台默认”改成“业务默认”并用数字验证每一轮调用是否可控。这篇文章面对的是正在接入 LLM API、开发 LLM Agent、或者维护 LLM 网关的开发者。理解 Counter-Defaults 后你能解释为什么默认参数会导致输出不稳定、费用超预算和 Agent 死循环也能在代码层面设计一套“反默认 计数器”的最小管控机制。示例采用 OpenAI 兼容接口但思路适用于绝大多数 LLM 平台。1. 默认值为什么是 LLM 应用失控的起点1.1 默认值代表服务方的通用选择不代表业务最优所有 LLM 服务商在提供 API 时都有一组默认参数。temperature 默认可能是 0.7、1.0max_tokens 可能是 16、256 或由模型决定top_p 默认是 1.0重试次数默认是 2 次。这些默认值面向的是“大多数通用对话场景”而不是你的具体业务。问题就出在这里如果你的代码没有显式传参那么每次调用都会静默使用服务商的默认值。代码看起来没有问题但实际生成行为完全由外部服务决定。等线上反馈“回答太长”或“总是重复”时你才意识到默认参数和业务需求不匹配。Counter-Defaults 的第一层含义就是“反默认配置”在代码或配置层明确写出每个关键参数的取值。这不仅是为了覆盖平台默认值更是为了让每次调用的行为可预期、可审计、可回滚。1.2 缺少 Counter-Defaults 的典型事故下面这些现象在 LLM 项目中反复出现根因几乎都和“默认值”有关。第一个典型场景是费用失控。开发者没有设置输出长度上限模型为了把回答写完整一次生成几千个 token。调用量一大账单立刻超出预期。要解决的不只是降 temperature而是必须给“单次输出长度”和“累计 Token 消耗”都加上计数器。第二个场景是输出风格漂移。同一个 Prompt 在 ChatGPT 网页里回答精炼在 API 里却变成“首先、其次、最后”的长篇大论。这是因为网页端和 API 默认参数不一样系统提示词也不一样。你写的是业务调用却没有显式声明系统提示词模型会按照训练阶段学到的通用“助手偏好”来组织语言。第三个场景是 Agent 死循环。LLM Agent 循环如果没有设置最大步数一旦模型反复调用同一个工具比如翻页抓取、查询接口就可能一直循环到超时。默认情况下很多框架不会限制工具调用次数这也是一种“默认无边界”。1.3 Counter-Defaults 的两层含义Counter-Defaults 可以拆成两个部分理解。第一层是 Counter 的“对抗”含义针对模型服务商提供的默认配置主动设置业务侧参数。比如对分类任务强制 temperature0.1对摘要任务至少设置 max_tokens512对 Agent 循环设置 max_steps10。这层工作解决的是“质量可控”。第二层是 Counter 的“计数器”含义在调用层维护 Token 计数、请求次数、成本估算和额度上限。当累计消耗超过阈值时直接熔断或告警。这层工作解决的是“成本可控”。两层缺一不可。只对抗默认参数不计数可能被单次超长输出击穿只做计数不调整默认参数又无法解决模型输出风格不稳定的问题。2. 需要对抗的常见默认行为与参数2.1 采样参数temperature、top_p 与惩罚系数生成类模型的采样参数直接决定输出的随机性。理解它们是设置反默认值的前提。temperature 控制概率分布的平滑程度。值越低模型越倾向于选择高概率 token输出更确定值越高随机性越强。常见默认值是 0.7 或 1.0。对于分类、抽取、代码生成任务这个值偏高对于创意写作则可以适当提高。top_p 控制核采样范围默认通常是 1.0相当于不限制候选集。它和 temperature 可以同时使用但不要同时大幅度调整。更稳妥的做法是固定一个微调另一个。frequency_penalty 和 presence_penalty 用来影响 token 的重复趋势。frequency_penalty 按“已经出现过的频率”惩罚重复词presence_penalty 按“是否出现过”鼓励新主题。默认值通常是 0。长文本摘要场景可以适当上调 frequency_penalty避免模型不停重复同一个观点。下面是一组常见配置速查表。参数常见默认值反默认策略适用场景temperature0.7 或 1.0分类/抽取用 0.1-0.3需要稳定输出temperature0.7 或 1.0创意写作用 0.7-0.9需要多样性top_p1.00.8-0.9降低低概率噪声max_tokens平台差异大按业务设置 128/256/512控制单次输出frequency_penalty0.0长文摘要设 0.2-0.5减少重复presence_penalty0.0头脑风暴设 0.1-0.3鼓励新话题这里要特别注意不同服务商的默认温度可能不同。同一个模型在推理服务框架里重新部署后默认值也可能被改成 1.0。所以不要依赖“平台默认值”所有参数都应在调用层显式传入。2.2 生成长度上限max_tokens 不等于模型最大长度很多新手以为 max_tokens 填模型的最大上下文长度就行这是错误理解。上下文长度包括输入和输出而 max_tokens 控制的是“本次生成的最大 token 数”。如果 max_tokens 设置过大模型可能写出一大段冗余内容却仍然没有真正回答问题。如果设置过小回答会被截断甚至出现在句子中间中止的情况。Counter-Defaults 的做法是按任务类型给输出长度设定边界。短问题回答128 到 256。邮件草稿256 到 512。长文档摘要512 到 1024。代码生成256 到 768。这里最关键的不是选一个准确数字而是“必须显式设置”。即使你不知道最佳值先设置一个保守值也比完全依赖默认值更容易排查问题。2.3 系统提示词缺失时的默认倾向系统提示词是控制模型行为最直接的手段。很多 API 调用只写 user 消息不写 system 消息这时模型会默认表现得像一个“通用助手”。通用助手的默认倾向包括过度解释、使用套话、先给出免责声明、倾向于说“作为 AI 助手我不能……”。这些倾向在聊天场景可以接受在自动化任务中却是灾难。Counter-Defaults 要求每个业务场景都提供明确的系统提示词。系统提示词里至少包含角色边界例如“你是订单客服”。输出规则例如“只输出 JSON不要解释”。内容禁止项例如“不要编造订单号”。风格约束例如“用不超过三句话回答”。系统提示词本质上是“提示层的人类默认值”用来覆盖模型训练阶段形成的通用默认行为。2.4 Agent 循环与工具调用的默认无边界LLM Agent 的典型结构是模型判断是否调用工具工具返回结果模型再次判断。如果缺少停止条件这个循环可能持续很多轮。默认情况下很多 Agent 框架不会限制工具调用次数。实际项目的反默认设置包括max_steps限制模型最多执行多少轮“思考-调用工具”max_tool_calls限制工具调用总次数idle_timeout限制单次工具等待时间total_token_limit限制整个会话的累计 token 消耗budget_limit限制单次任务的估算费用。这些值必须在 Agent 启动时传入而不是在循环体里手动 else break。把限制放在框架参数层可以让所有分支都受到约束。2.5 精度与运行环境的选择FP16、FP32、BF16热词里反复出现“LLM 大模型之精度问题FP16、FP32、BF16”这和 Counter-Defaults 也有关系。模型推理时的数值精度会影响输出质量、显存占用和延迟它是模型服务层一种容易被忽略的“默认配置”。FP32 是单精度浮点数精度高、显存占用大、推理速度相对慢。FP16 是半精度浮点数显存占用减半但数值范围小训练过程中容易溢出推理阶段也可能出现细微的精度损失。BF16 是 Brain Floating Point指数范围和 FP32 一致精度位数更少但更适合大模型推理是目前很多推理框架的默认选择。反默认配置的含义不是“必须用 BF16”而是针对自己的任务做验证。如果某些任务在 FP16 下结果不稳定可以改用 BF16 或 FP32。如果对显存要求高FP16 和 BF16 多出的速度优势可能更值得。精度选择属于部署层也应该显式记录在模型服务配置中而不是等出现问题后再猜。精度类型位数特点常见使用场景FP3232 位精度高显存占用大小模型、调试精度问题FP1616 位显存减半数值范围有限部分 GPU 上的快速推理BF1616 位动态范围和 FP32 一致大模型训练与推理3. 搭建最小可运行环境并落地反默认配置3.1 环境准备与依赖安装下面用 Python 实现一个最小客户端。你需要 Python 3.9 以上环境并安装 openai 库。pip install openai如果你的 LLM 服务是 OpenAI 兼容 API可以直接通过 base_url 指向本地推理服务或网关。先检查是否已经设置 API Key。export OPENAI_API_KEY你的-api-key export OPENAI_BASE_URLhttps://你的-gateway地址/v1这里要注意不同平台的 base_url 路径不同。有的网关要求带 /v1有的不需要。可以先在命令行用 curl 做冒烟测试确认接口通后再写代码。3.2 统一客户端封装我们可以写一个LLMCounterDefaultClient把“反默认参数”和“计数器”内聚到同一个对象里。这样每个调用处都不需要重复设置参数也不会因为漏传某个参数而回退到平台默认值。from openai import OpenAI class LLMCounterDefaultClient: def __init__( self, model: str, api_key: str, base_url: str | None None, temperature: float 0.3, max_output_tokens: int 512, top_p: float 0.9, frequency_penalty: float 0.2, presence_penalty: float 0.0, max_retries: int 2, request_timeout: float 30.0, token_budget: int 100000, ): self.client OpenAI( api_keyapi_key, base_urlbase_url, timeoutrequest_timeout, max_retriesmax_retries, ) self.model model self.temperature temperature self.max_output_tokens max_output_tokens self.top_p top_p self.frequency_penalty frequency_penalty self.presence_penalty presence_penalty self.token_budget token_budget self.token_used 0 self.request_count 0 self.last_usage None def chat(self, messages, **kwargs): if self.token_used self.token_budget: raise RuntimeError(ftoken budget exhausted: used{self.token_used}, budget{self.token_budget}) params { model: self.model, messages: messages, temperature: kwargs.get(temperature, self.temperature), max_tokens: kwargs.get(max_tokens, self.max_output_tokens), top_p: kwargs.get(top_p, self.top_p), frequency_penalty: kwargs.get(frequency_penalty, self.frequency_penalty), presence_penalty: kwargs.get(presence_penalty, self.presence_penalty), } response self.client.chat.completions.create(**params) usage response.usage self.last_usage usage self.token_used usage.total_tokens self.request_count 1 if self.token_used self.token_budget: raise RuntimeError(ftoken budget exceeded after request: used{self.token_used}, budget{self.token_budget}) return response.choices[0].message.content def remaining_tokens(self): return self.token_budget - self.token_used def reset(self): self.token_used 0 self.request_count 0 self.last_usage None这个类的核心不是“能用”而是“每个关键参数都有显式默认值”。即使调用方不传任何参数也不会悄然落在平台默认值上。3.3 参数模板设计单一客户端无法覆盖所有任务所以可以在客户端之上维护多套“反默认参数模板”。例如PROMPT_TEMPLATES { classify: { system_prompt: 你是一个分类器。只输出类别名不要解释。, temperature: 0.1, max_output_tokens: 32, }, extract: { system_prompt: 从文本中抽取关键信息以 JSON 格式输出。, temperature: 0.0, max_output_tokens: 256, }, summary: { system_prompt: 用三句话总结用户输入不要添加原文没有的信息。, temperature: 0.3, max_output_tokens: 256, frequency_penalty: 0.3, }, creative: { system_prompt: 你是一个创意写作助手。, temperature: 0.8, max_output_tokens: 768, }, }每个模板都包含 system_prompt 和采样参数。调用时按业务场景选择模板而不是用同一种温度处理所有请求。4. 用计数器约束 Token、成本与循环边界4.1 在客户端维护 Token 计数前面代码中的token_used就是最低限度的计数器。每次请求后从响应里的usage.total_tokens累加消耗。client LLMCounterDefaultClient( modelgpt-4o-mini, api_keysk-xxx, base_urlNone, temperature0.2, max_output_tokens200, token_budget1000, ) resp client.chat([ {role: system, content: 只回答事实不要输出多余寒暄。}, {role: user, content: 解释什么是 FP16。}, ]) print(resp) print(used tokens:, client.token_used) print(remaining:, client.remaining_tokens()) print(requests:, client.request_count)如果连续多次调用token_used会逐步攀升。当累计值超过 token_budget 时客户端会拒绝新的请求而不是让账单继续增长。这里有一个容易忽视的点usage.total_tokens包含输入和输出。如果你只关心输出 token可以单独读usage.completion_tokens。成本计算通常需要分别记录prompt_tokens和completion_tokens因为两者的单价不同。4.2 请求频率与并发控制Token 计数器只能控制“总量”控制不了“速率”。如果某个任务循环调用 LLM可能一分钟内发出上百个请求触发服务端限流。生产环境至少需要两个指标每秒请求数 QPS每分钟 token 消耗速率。在客户端可以用简单的令牌桶算法控制 QPS。例如import time class RateLimiter: def __init__(self, max_calls_per_second5): self.min_interval 1.0 / max_calls_per_second self.last_call_time 0.0 def wait(self): now time.monotonic() interval now - self.last_call_time if interval self.min_interval: time.sleep(self.min_interval - interval) self.last_call_time time.monotonic()把rate_limiter.wait()放在每次 API 调用前可以有效减少 429 限流错误。不过它只做本进程控制多实例部署时还要依赖 Redis 或网关层的分布式限流。4.3 网关层统一配额管理客户端计数器适合小团队和单机应用但无法覆盖多服务、多模型、多团队的使用场景。当多个后端服务都调用 LLM 时每个服务的预算独立很难统一控制。这时需要引入 LLM Gateway。网关层可以记录每个应用的 API Key、每分钟请求数、每日 Token 消耗、单次请求的模型和参数。一旦超过配额直接返回 429 或自定义错误码。LLM Gateway 的 Counter-Defaults 落地方式包括为每个业务线设置 token 日配额按模型拆分成本例如普通问答用便宜模型复杂任务用强模型日志中记录 temperature、max_tokens 等实际参数参数缺失时网关自动补充业务模板参数而不是透传到模型服务。这些能力不需要一次性全部实现。先在客户端统一封装再逐步把计数和限制前移到网关。5. 运行验证与常见问题排查5.1 最小验证用例写完客户端后不要只验证“能返回文本”。建议验证三个层面内容是否正确、计数是否准确、异常是否能触发。第一个验证用例resp client.chat([ {role: user, content: 请只回答一个字是}, ]) print(resp)预期结果是输出“是”或“是的”而不是一大段解释。如果输出了长文本说明系统提示词或温度设置没有覆盖住模型的默认长篇倾向。第二个验证用例client.reset() client.token_budget 50 try: client.chat([{role: user, content: 请写一篇 500 字说明文}]) except RuntimeError as e: print(触发预算熔断:, e)这里把预算调低到 50目的是确认计数器逻辑生效。正常调用应该触发 RuntimeError而不是让请求继续执行。5.2 预期输出与异常路径运行时可能遇到几种典型异常openai.AuthenticationErrorAPI Key 错误或权限不足openai.RateLimitError触发限流需要退避重试openai.BadRequestError参数不合法通常是 max_tokens 超过模型上限、messages 格式错误TimeoutError请求超时。排查顺序应该是先确认 API Key 和模型名是否正确。再确认 base_url 是否指向正确网关。检查 messages 里是否有空字符串或角色错误。检查参数是否超出模型允许范围。最后看原始异常信息而不是只看封装后的提示。很多“跑不通”的问题都是因为复制了旧代码模型或参数被平台废弃。5.3 常见问题排查表问题现象常见原因检查方式处理建议回答总是很长未设置 max_tokens 或系统提示未约束长度查看调用日志中的实际参数设置 max_output_tokens 并添加“简短回答”提示回答不稳定temperature 默认值偏高打印请求参数按任务模板设置 temperature 0.1-0.3回答被截断max_tokens 设置过小查看 finish_reason 是否为 length调大 max_tokens 或压缩提示词Agent 死循环未设置 max_steps查看 Agent 日志中的工具调用次数在框架层设置最大步数和总 token 限制Token 统计与账单不一致忽略输入 token 或 prompt 缓存分别统计 prompt_tokens 和 completion_tokens按计费口径记录指标不要只看 total_tokens偶尔出现明显错误答案精度问题或采样参数不合适固定 seed 对比多次结果部署层验证 FP16/BF16应用层降低 temperature5.4 精度问题导致的隐性不稳定如果应用层参数已经固定但推理结果仍然忽好忽坏要检查模型服务端部署时的精度设置。同一个模型在 FP16 和 BF16 下逐 token 概率分布会有细微差异。在需要严格复现的场景中可以固定随机种子并降低 temperature但不一定能完全消除精度影响。更有效的做法是建立回归集准备 20 到 50 条典型请求记录每条请求的输出哈希部署版本升级后对比差异。如果关键任务输出变化超过预期就回到部署配置检查精度和框架版本。6. 生产环境最佳实践与扩展方向6.1 配置外置化和多环境隔离不要把 temperature 和 token_budget 硬编码在业务代码里。生产环境的推荐做法是把这些参数放到配置文件或配置中心。llm: model: qwen2.5-7b-instruct base_url: ${LLM_BASE_URL} temperature: 0.2 max_output_tokens: 512 top_p: 0.9 token_budget: 1000000 qps: 5通过环境变量注入 API Key避免把密钥提交到仓库。测试环境和生产环境使用不同的 token_budget。测试环境可以调小预算让超限问题尽早暴露。6.2 从单次调用走向 LLM 编排框架当业务从“单次问答”升级为“RAG 问答”或“Agent 任务”Counter-Defaults 不再只是客户端参数问题而是编排框架的全局约束。LLM 应用为什么需要编排框架因为你需要同时管理用户输入、检索结果、工具调用、模型输出、上下文长度、失败重试。框架可以帮你把提示词拼接和工具调用串起来但它不会自动帮你设置安全边界。所以在引入 LLM 编排框架时仍然要显式覆盖这些默认值检索返回的上下文数量每次检索注入的最大字符数Agent 最大迭代次数单轮工具调用超时整个会话的 token 上限。这些值是编排层的“反默认配置”应在业务启动时统一注册。6.3 MCP 与 Agent 场景下的 Counter-Defaults热词中多次出现 MCP Client 与 LLM 连接。MCPModel Context Protocol让 LLM 通过统一协议调用外部工具但它同样存在默认行为问题。连接 MCP 后模型可以访问的工具列表通常是由服务端定义。如果不做限制模型可能调用与当前任务无关的工具增加延迟和安全风险。Counter-Defaults 的做法包括按任务过滤工具白名单设置单次任务的工具调用次数上限限制工具返回内容大小避免上下文被撑爆记录每次工具调用的参数和耗时用于审计。实现 MCP Client 与 LLM 连接时不要只关注“能不能连上”还要确认“模型能否在明确约束下调用工具”。6.4 可复用的落地检查清单最后整理一份可以直接用于代码审查的清单。[ ] 所有 LLM 调用都显式传入 temperature、max_tokens、top_p而不是依赖平台默认值。[ ] 每个业务场景都有独立 system prompt明确输出格式和禁止项。[ ] 客户端维护 token_used 和 request_count并提供预算熔断。[ ] Agent 循环设置了 max_steps、max_tool_calls、total_token_limit。[ ] 生产环境通过配置中心管理模型参数密钥通过环境变量注入。[ ] 日志中记录了实际请求参数、模型名称、token 消耗和耗时。[ ] 模型服务端记录推理精度FP16/BF16/FP32和框架版本。[ ] 关键任务有回归用例每次模型或部署版本更新后执行对比。[ ] 网关层有按团队、按模型拆分的配额和告警。把 Counter-Defaults 变成系统设计的一部分后LLM 应用从“能跑”走向“可控”。下一步可以按照调用量接入监控面板、成本告警以及按业务线拆分额度让每个团队都为自己使用的默认参数负责。真正重要的不是记住某个参数的最佳值而是让每一次模型调用都有明确的业务预设和可验证的数字边界。