第一次接 LLM API 时,我其实没怎么认真看参数,默认温度是多少就让它跑多少。结果让模型输出一个 JSON 格式的摘要,它总是额外补两句解释,偶尔还直接用 Markdown 反引号把 JSON 包起来。当时我的第一反应是“模型不够聪明”,后来才发现,问题不是模型,而是我在顺着默认走。
几乎所有 LLM 产品都塞满了“默认”:默认温度、默认上下文窗口、默认把一次生成结果当作最终答案、默认把整个知识库直接塞进提示词、默认让 Agent 自己决定要不要调用工具。这些默认值并不是失误,它们更像是服务商为了覆盖最大公约数场景设计的平衡点。换句话说,默认值不是给你定制的,是给所有人跑通用的。
所以我越来越认同一个思路:在使用 LLM 时,真正拉开差距的,不是你有没有用上更强的模型,而是你有没有主动识别出“默认在哪里拖了你的后腿”。我习惯把这个过程叫作LLM Counter-Defaults——反默认。注意,它不是“把所有参数都改掉”的叛逆,也不是听别人说温度调低 0.1 就一定更好。它是让你先有能力看见默认,再按场景判断该保留什么、该反转什么,并且用实验验证调整后的结果真的变好了。
这篇文章,我会把我自己的反默认经验整理成三层:第一层是采样参数和数值精度,第二层是提示词与工作流编排,第三层是工程化和排查。每一层都不是孤立存在的,最终都会落到同一个判断上:你的 LLM 应用,是碰运气跑通,还是设计出来跑通。
1. 不要急着调参,先搞清楚 LLM 里的“默认”是什么
1.1 默认值不是给你定制的,是给所有人都能跑通用的
很多刚接触 LLM 的同学,会有一个误解:默认参数既然由官方或框架作者设置,那大概率是“最优参数”。但如果你去读各大平台的文档,会发现默认值的设定逻辑通常很简单——保证大多数请求不报错、不返回空、不超出上下文限制。
举个例子,很多平台的默认温度在 0.7 到 1.0 之间。这个范围对创意写作、头脑风暴是有帮助的,因为模型输出会有更多变化。但如果你做的是一个“从合同里抽取关键字段” 的任务,温度 1.0 意味着同一个合同每次抽出来的结果可能都不一样。你的第一反应不应该是骂模型不稳定,而是思考:这个场景真的需要默认温度吗?大概率不需要。
我见过不少项目,在没动任何参数的情况下,把 LLM 当作 API 直接接到业务里。结果线上出现三种典型问题:
- 输出格式不稳定,有时候是合法 JSON,有时候多了提示性前缀。
- 同一个问题反复问,答案差别很大,导致下游逻辑一言难尽。
- 成本没有边界,因为默认 max_tokens 可能很大,模型总有几个 token 的空转输出。
这些问题的共同根源,都是“默认”被当成了“不用思考的配置项”。反默认的第一步,其实是把默认值从“看不见的背景”里捞出来,逐个问一遍:这个默认项在我的场景下,合理吗?
1.2 真正的反默认,是重新定义你的输入、输出和成功标准
调参之前,先要做一件更重要的事:定义你想要的“成功”。否则你只是把参数的旋钮从左拧到右,并不知道哪里算好。
拿我做过的知识库问答工具举例。一开始我默认的流程是:用户提问 -> 把相关文档拼进提示词 -> 让大模型直接回答。这个流程有三层默认假设:
- 所有相关资料都能被准确找到。
- 模型有能力一次性读完并理解所有拼接内容。
- 模型生成的回答可以直接展示给用户。
这三条假设,在真实业务里几乎都会出问题。
所以后来我做了三件反默认的事。先不急着让模型回答,而是把文档切成块,只检索 TopK 相关块进入上下文;再在提示词里要求模型必须引用来源编号;最后增加一道输出校验,检测回答里是否真的包含引用标记,如果没包含,就重试一次或退回兜底提示。这不是什么高深技术,但它改变的是“默认流程”:
- 输入从“尽可能多的资料”变成“经过筛选的最少相关资料”。
- 输出从“一段自由文本”变成“带来源标记、可校验的文本”。
- 成功标准从“看起来回答了”变成“格式正确、有引用、命中评估集”。
这就是反默认的实质。它不是某几个参数的小改动,而是重新设计输入、输出和成功标准,让 LLM 的随机能力被装进一个可控的壳里。
2. 从 API 参数到推理引擎,反默认的第一层:采样与数值
2.1 温度、top_p、max_tokens 这些默认值,到底在控制什么
这一层是大多数人最先接触的,也是最好入手验证的。
先说温度(temperature)。它控制输出分布的随机程度。温度越高,模型越容易选择概率更低的 token,输出更多样,但也更容易飘。做结构化抽取、分类、代码生成,我通常会把温度调到 0.2 甚至更低。做文案改写、创意脑暴,才考虑把温度调高到 0.8 以上。
再看 top_p,也叫核采样。它代表在累积概率达到该阈值的最小 token 集合里采样。默认值通常在 0.9 或 1.0。它和 temperature 不是两个独立维度,而是都会影响采样随机性。实践中,我更建议先固定一个,只调另一个。否则同时调,出了问题很难定位是谁引起的。
max_tokens 是输出长度上限。很多平台的默认值,对通用问答来说都够用,但对结构化输出任务,容易造成“回答到一半被截断”。反过来,如果只是做短标签分类,默认值又可能太大,白白浪费 token 和延迟。
stop 参数也很容易被忽略。很多平台默认不设置停止符,导致模型经常会输出一些“解释的话”。如果你的任务明确要求输出 JSON,可以在 stop 里配置 JSON 结束标志,比如}或换行符。不过要注意,有些结束符会误伤输出内容,使用前需要在样例集上验证。
seed 是另一个反默认的好工具。部分平台支持固定随机种子,让相同输入得到更稳定的输出。注意,它不能绝对保证完全一致,但对调试和回归测试非常有帮助。
我整理了一张简单的参数核对表,可以复制到自己的项目文档里:
| 参数 | 默认行为 | 常见风险 | 反默认建议 |
|---|---|---|---|
| temperature | 偏高,0.7~1.0 | 结构化任务输出不稳定 | 抽取/分类用 0.1~0.3,创意生成用 0.7~1.0 |
| top_p | 0.9 或 1.0 | 与 temperature 同时调,出问题难定位 | 固定一个,只调另一个 |
| max_tokens | 平台默认,可能过大/过小 | JSON 被截断,或浪费 token | 按任务最小必要长度设置 |
| stop | 默认无 | 模型输出多余解释 | 对固定格式任务,配置结束符 |
| seed | 默认随机 | 调试不可复现 | 进入联调阶段后固定 seed,做对照测试 |
2.2 精度模式:fp16、fp32、bf16 不是单纯精度选择,是质量与成本权衡
热搜词里有一项是“LLM 大模型之精度问题(fp16, fp32, bf16)详解与实践”。精度问题看起来只是本地推理或训练时的细节,但它同样是一种默认。
在很多推理引擎里,默认会用半精度 fp16 来加速计算、减少显存占用。fp16 的问题在于动态范围比较窄,某些数值很容易溢出或损失精度。bf16 则把指数位做得和 fp32 一样宽,保留了更大的数值范围,但尾数精度低。fp32 最稳妥,但显存开销和计算成本也最高。
那么普通应用开发者需要关心吗?如果你只是调用别人的 API,通常不需要关心,因为服务端已经帮你选了精度。但如果你在本地部署开源模型,或者自己写推理脚本,精度/量化级别就是默认值之一。
从工程经验看,通常先在默认精度下跑通流程,记录输出质量。再用你的测试集切到不同精度,对比结果。如果你做的是摘要、问答、翻译这类语义任务,fp16 和 fp32 的差异可能不明显,那优先选更省显存、速度更快的方案。如果你做的是数学推理、代码逻辑判断等对数值敏感的任务,建议保守一点,保留更高精度或更低保真损失的量化方案。
这里没有“必定最优”的答案,但可以给一个通用顺序:
- 用默认精度跑 20 条代表性 case。
- 记录格式通过率和语义正确率。
- 切换到候选精度,再跑同一批 case。
- 如果结果差异在可接受范围内,选择更省资源的方案;如果差很多,就继续用稳健方案。
2.3 一个可复用的参数摸底方法
反默认最怕的是“凭感觉调参”。我自己的做法是建一个极小的参数摸底脚本。
核心思路是:准备一份覆盖主要场景的测试题,固定一个基准提示词,只改变一个参数,跑完后记录四个指标:结果是否符合预期、格式是否通过、耗时、token 消耗。不需要做复杂的统计,只要能把“温度从 0.2 改成 0.7 到底发生了什么”用数据讲清楚。
示例结构如下:
# 示例结构,不是生产代码 cases = load_test_cases("test_cases.jsonl") configs = [ {"temperature": 0.2, "top_p": 0.9, "max_tokens": 512}, {"temperature": 0.7, "top_p": 0.9, "max_tokens": 512}, {"temperature": 1.0, "top_p": 1.0, "max_tokens": 512}, ] for cfg in configs: for case in cases: response = call_llm(case.prompt, **cfg) record(case.id, cfg, response, evaluate(case, response))关键不是这个脚本写得多优雅,而是你要遵守一条纪律:一次只改一个变量。否则你调完 temperature 和 top_p,发现结果变了,根本不知道是哪一步起了作用。
3. 反默认的第二层:从单次调用到工作流编排
3.1 把提示词“焊死”成模板,而不是每次都靠临场发挥
很多初版应用,提示词是散落在代码里的字符串拼接。用户问一句,程序就把问题塞进一个 text 模板,发给模型。这种写法不是不能用,而是很难迭代。
默认思维是:提示词是一次性的输入。反默认思维是:提示词是你的程序代码的一部分,应该被独立维护、版本控制、测试。
一个可复用的提示词模板,至少要包含这些部分:
- 角色与任务:告诉模型它是什么角色,要完成什么。
- 输入定义:把用户的问题、检索到的资料、字段说明放到固定区块里。
- 输出约束:明确格式、长度、引用规则。
- 少量示例:对复杂格式,提供 1 到 2 个示例。
示例模板结构:
你是知识库问答助手。 请根据<资料>中的内容回答问题。 如果资料不足以回答,请直接说“资料不足”,不要编造。 <资料> {retrieved_chunks} </资料> 问题:{user_question} 要求: 1. 回答不超过 200 字。 2. 每句话末尾标注来源编号,例如[1]。 3. 只输出回答正文,不要输出任何解释。把提示词模板从代码里抽出来,你会发现后续的调参、评估、回归会顺很多。因为你改的是同一个入口,而不是在业务代码里找字符串。
3.2 默认直接出结果,但生产流程需要校验、重试和异常路径
另一个常见默认是:调用一次 LLM,拿到结果,直接作为接口响应返回。对原型来说够了,但生产环境往往不够。
以输出 JSON 为例。哪怕你把温度调到 0.1,也不能保证模型永远输出合法 JSON。因为 token 采样是概率性的,没有任何参数能保证 100% 格式正确。
反默认的做法是增加一道校验层。先判断结果能不能被解析;如果解析失败,是直接报错,还是用新提示词让模型修复,还是再调用一次?这三种策略的成本和效果都不同。你可以先做简单的重试,比如最多重试两次;重试还不成功,就返回一个固定兜底结构。
同样的逻辑也适用于 Agent 编排。
热词里出现了 “LLM Agent”“MCP” 和 “LLM 应用为什么需要编排框架”。默认的 Agent 框架通常会允许模型在循环里反复调用工具,直到它认为自己完成了任务。这个默认设计让 Agent 很灵活,但也带来风险:模型可能在工具调用里绕圈,或者因为上下文太长而失去方向。
反默认的做法是给 Agent 加约束:
- 设置最大调用轮数,比如 5 轮。
- 每个工具调用前都要有白名单校验,不暴露全部工具。
- 关键操作,比如删除、写入、花钱,需要人工确认。
- 记录每次工具调用的输入和输出,方便事后回溯。
这也回应了热词里提到的“过度授权”风险。LLM API 给工具赋予了“能做什么”的能力,但真正决定“应该做什么”的,必须是你自己设定的边界。反默认不是限制工具的灵活度,而是把灵活度控制在你可接受的范围里。
3.3 检索增强和 Agent 编排,反掉“只要模型够强就行”的默认
很多人以为,模型越强,就越不需要做检索和流程设计。这个默认想法在 demo 阶段是成立的,但真实业务里会遇到两个问题:
- 模型上下文窗口再大,也装不下整个知识库。
- 模型很容易在长上下文里遗漏关键信息,甚至被无关信息干扰。
所以 RAG(检索增强生成)才变得重要。但 RAG 自己也有默认值。
向量检索是 RAG 的默认主力。它对语义相似度很有效,对精确数字、缩写、产品型号这类文本却经常不友好。比如你问“项目编号是 ABC-1024 的文档在哪里”,纯向量检索可能找不到完全匹配,因为它更擅长找“上下文含义相近”的内容。
反默认的做法是混合检索。把向量检索和关键词检索的结果合并,再用一个重排模型或规则排序,最后取 TopK 进入上下文。这样做会多花一点时间,但能显著减少“明明有答案却找不到”的情况。
同样,Agent 编排也有默认问题。默认让 Agent 完全自主决定下一步,对一个不常用、没有人工监督的小任务还好,但一旦任务链路变长,你就很难判断它为什么跳到了某个操作。反默认思路是:能写成显式流程的步骤,就不要让模型自由发挥。模型只负责真正需要理解和判断的部分,比如“这段用户意图,应该走哪个分支”。这样既保留了灵活性,又没有把整个系统变成黑盒。
热词里的“LLM Wiki”也值得提一句。很多人尝试让 LLM 把笔记自动整理成 Wiki 条目,默认路径是“丢一堆文档给模型,模型输出整理结果”。这种做法做出来的 Wiki 往往充满幻觉和错误归纳。反默认做法是让 LLM 只负责从原文抽取候选条目和链接关系,然后由人确认,再写入知识库,并保留源文链接。知识图谱里的每个节点,都应该能回到原文。否则你建的只是一个“看起来很有条理的幻觉库”。
3.4 MCP、工具调用和 RAG,如何选择适合自己场景的默认链路
近两年,MCP(Model Context Protocol)这类标准协议让 LLM 连接外部工具变得更方便。默认链路通常是:LLM -> MCP Client -> MCP Server -> 具体工具。
但这里有个容易被忽略的点:协议只解决“能不能连”,不解决“该不该暴露”。默认把所有工具都注册给模型,从模型的角度看是方便,从系统安全角度看是风险。反默认的做法是:按任务范围只暴露最小工具集合。比如,一个只负责查天气的应用,不需要让模型看到文件删除接口。
你在选择编排框架时也要注意,框架的默认封装既可能是糖,也可能是坑。比如有一些框架默认开启了自动重试、默认缓存、默认 Agent 循环,这些在你没意识到的时候就会产生额外 token 成本或奇怪行为。所以无论你用 Spring AI、LangChain、LlamaIndex 还是自研流程,都要先看清楚框架在你背后到底做了什么,再决定要不要保留。
还有一个热词问题:“ComfyUI 与 LLM 必须在同一台电脑上么?”这其实也是一种默认。很多人会在本地跑 ComfyUI 做图像工作流,同时也想在本地调用 LLM 做提示词解析,于是默认认为必须装在同一台机器上。不是的。
只要网络能互通,LLM 服务可以跑在远程服务器,ComfyUI 可以跑在本地工作站,两者通过 HTTP API 或消息队列通信。更常见的方案是:LLM 部署在有 GPU 的服务器,ComfyUI 跑在另一台有显卡的机器,然后通过 API 调用。关键不是“同一个电脑”,而是“延迟和带宽是否满足你的工作流节奏”。如果 LLM 响应延迟不高,完全可以拆开部署。
4. 反默认的第三层:从个人实验到多人协作的工程化
4.1 默认的本地路径、API Key、模型版本都藏着隐患
当 LLM 应用从个人脚本变成团队项目,反默认的范围就不只是参数和工作流了,还包括配置和依赖。
默认习惯是把 API Key 写在代码里,把本地文件路径直接写死,模型名称填一个测试版本。一个人开发没问题,一旦多人协作,这些默认就会变成事故源:
- API Key 被提交到 git 仓库,泄露风险极高。
- 本地路径在另一台机器上根本跑不通。
- 模型版本悄悄从
model-a升到model-a-20250201,同一条提示词输出全变了。
反默认做法并不复杂:
- 所有密钥从环境变量或密钥管理服务读取。
- 所有路径通过配置对象传入,不写死。
- 生产环境固定模型版本,模型升级要走评估流程,而不是默认跟随最新。
4.2 日志、版本、评估集:把默认的“看不见”变成“可追踪”
LLM 应用的可观测性,是工程化里最容易被跳过的一环。默认状态下,你只看到最终返回给用户的结果,中间发生了什么一概不知。
反默认的做法是给每次请求留痕。至少应记录:
- 请求 ID 或会话 ID。
- 输入的 prompt 模板版本。
- 实际发送给模型的完整消息(注意隐私,脱敏后再落日志)。
- 模型返回的原始内容。
- 参数配置:温度、max_tokens、模型版本。
- 耗时、token 用量、重试次数。
- 最终是“通过校验”还是“走重试/兜底”。
有了这些日志,你才能回答三个问题:为什么这个回答这么差?为什么这次调用这么慢?为什么今天成本比昨天高?
同时,要建一个小型评估集。哪怕只有 30 到 50 条也不嫌少。每次调整提示词或参数后,都用同样的评估集跑一遍,对比结果。如果没有评估集,你会陷入“调好 A 类问题,搞坏 B 类问题”的死循环。
4.3 一个最小可用的 LLM 应用基线模板
反默认并不意味着从零造轮子。我建议大多数项目从最小可用流程开始,先跑通,再逐步加固。下面这个模板可以当作基线,然后按业务情况补强:
- 输入校验:检查用户输入的长度、字段、格式。
- 提示词构建:从模板文件读取 prompt,并注入统一变量。
- 调用 LLM:固定模型版本、温度、max_tokens、seed 等参数。
- 输出校验:按任务要求解析格式,失败则按策略重试。
- 异常处理:重试失败后走兜底逻辑,不把异常直接抛给用户。
- 日志记录:保存请求参数、响应、耗时、token 成本。
- 评估回归:在合并到主分支前,跑一次评估集。
这个模板本身并不复杂,但它把默认的“单次调用”变成了一个可观察、可控制的流程。之后的优化,都发生在这些步骤的内部,而不是靠“重新发明一套”。
5. 反默认之后,问题排查链路怎么走
5.1 现象与输入:先判断是模型没懂,还是流程断掉
反默认做得越多,系统越复杂,出错的可能也越多。排查问题时,我习惯按顺序走。
第一步是看现象。错误通常分几类:
- 直接报错:可能是 API Key 无效、网络不通、依赖版本冲突。
- 卡住不返回:可能是超时设置太短,也可能是上下文太长。
- 返回空:可能是 stop 参数太早触发,也可能是 max_tokens 设为 0。
- 输出格式不对:可能是提示词没有约束,也可能是温度太高。
- 回答质量差:可能是输入上下文不完整,也可能是检索召回不准。
- 速度慢:可能是并发不够、上下文太长,也可能是量化/精度配置太保守。
- 成本高:可能是 max_tokens 太大,也可能是 Agent 循环太多。
第二步是查输入。很多时候问题不在模型,而在你给它的东西。检查提示词有没有被正确填充、上下文是被截断还是没拼进去、特殊字符有没有导致解析错误。这里最容易忽略的是编码问题,尤其是从文件里读文档时,中文和特殊符号可能会被破坏。
5.2 环境与参数:再查依赖、精度、并发和上下文窗口
如果输入没问题,再往环境层查。
先确认依赖版本。LLM 框架迭代很快,你本地用的版本和文档示例可能已经不一致。如果你的代码是三个月前写的,现在跑不起来,大概率是某个库做了破坏性更新。
再确认参数是否被“默认覆盖”。例如,你在代码里设置了temperature=0.2,但初始化客户端时用了某个平台默认参数,可能实际请求并没有带上你的配置。有的 SDK 在传参时如果字段名不对,会静默忽略。所以日志里一定要打印实际请求参数。
如果用了本地推理,还要检查精度和量化。有些量化级别在特定任务上表现很差,但不是所有量化都一样。如果输出劣化,先切回更高精度对比,再决定是否继续用量化。
并发和上下文窗口也是常见瓶颈。并发过高时,平台可能限流;上下文过长时,模型会“忘了”前面内容或直接截断。这类问题通常表现为“短文本正常,长文本开始乱答”。
5.3 工具边界:最后判断是不是当前方案根本不适用
最后一层,也是很多人不愿意承认的:当前任务可能根本不适合用 LLM,或者不适合用当前的复杂链路。
比如,用户输入是固定的枚举值,你完全可以用规则或字典匹配,却让 LLM 去“智能理解”,结果又慢又不稳定。再比如,需要精确计算金额的任务,应该用代码计算,而不是让模型做算术。还有,如果责任要求极高,完全依赖一次生成结果而不加人工审核,本身就是设计缺陷。
这时候反默认的终点,是“反掉非要用 LLM 这个默认”。不是所有问题都该用语言模型解决。能够用确定性逻辑解决的部分,优先用确定性逻辑;只有那些需要理解语义、生成自然语言、总结归纳的部分,才交给 LLM。这套“先拆任务,再选工具”的思路,比任何参数调优都更能提升系统稳定性。
6. 回到默认:所有的反默认,最后都要能回归验证
6.1 用对照实验代替拍脑袋
反默认不是一场“越改越复杂”的运动。每个改动都要有目的、有验证。我把这个过程总结成一个很朴素的三步循环:
- 先跑通默认,拿到一个基线。
- 每次只改一个变量,记录结果变化。
- 如果改动带来稳定提升,就把配置固化下来,否则回滚。
这个循环看起来简单,但很多人做不到。原因是他们不建立测试集,也不保存历史配置,于是只能永远“凭感觉调”。反默认能力的核心,不在于你知道要调低温度,而在于你知道自己为什么调低、调低后怎么验证。
6.2 适合与不适合反默认的场景
最后给一些边界判断。
反默认并不适合所有时刻。如果你只是做一个临时原型、一次性脚本,或有意识做创意发散,保留默认参数和默认流程,反而更高效。你没必要为一个明天就删的脚本搭日志和评估集。
但如果你面对的是这些情况,就值得认真做反默认:
- 输出要直接进入生产业务流程。
- 需要稳定复现,不能每次结果都猜。
- 多人协作,或长期维护。
- 成本敏感,需要对 token 消耗有预算。
- 需要审计和回溯,出了问题能定位。
LLM 依然是个概率系统,反默认不能让它变成确定性软件,但能让你在概率之上建立更稳健的工程边界。默认值给所有人一个统一的起点,而反默认是让你从起点走向自己业务真实目标的那条路。走不走、怎么走,才是真正体现工程水平的地方。