1. 从“能跑通”到“能讲清”大模型调用背后的认知断层我见过太多简历上写着“熟悉大模型应用开发”的人面试时让他们讲讲一次完整的推理请求到底经历了什么能说清楚的人不到三成。大多数人能熟练地写出client.chat.completions.create(...)能对着文档把参数一个个填进去但一旦问“temperature 和 top_p 同时设置时到底谁说了算”“为什么同样的 prompt 两次返回结果差异巨大”“流式输出在服务端到底是怎么拼出来的”就开始含糊其辞。这不是个别现象而是整个行业快速膨胀期的典型症状。大模型 API 的封装做得太好了好到把底下所有复杂性都藏了起来。你调用一个函数传进去一段文字几秒钟后拿到一段回复整个过程像魔法一样。但工程不是魔法工程是把魔法拆解成可理解、可控制、可优化的模块。会调 API 的人很多因为门槛确实低懂原理的人极少因为愿意往下钻的人少。这篇手记想做的事情很具体把一次大模型 API 调用从你按下回车到拿到结果之间发生的所有关键环节一层层剥开。不是讲论文里的 Transformer 架构也不是讲怎么训练模型而是讲推理服务这条链路上一个应用开发者必须理解的工程原理。理解这些之后你写出来的代码质量、排查问题的速度、优化成本的能力会和只会调 API 的人拉开明显差距。适合读这篇内容的人已经用过大模型 API 做过至少一个完整项目但总觉得“知其然不知其所以然”的开发者正在做 AI 应用架构设计需要判断哪些环节可以优化、哪些参数不能乱动的技术负责人以及那些面试时被追问“为什么”就卡壳、想补上这块认知短板的人。2. 一次推理请求的完整生命周期从你的代码到模型的输出2.1 请求发出之前Tokenization 决定了你真正发送了什么很多人以为发给模型的就是自己写的那段文字。不是的。你写的文字在离开你的进程之前已经被切成了 token。这个过程叫 tokenization是大模型推理链路里第一个被严重低估的环节。Token 不是字不是词也不是字节。它是介于字符和单词之间的一种子词单元。英文里 “unbelievable” 可能被切成 “un”“believ”“able” 三个 token中文里 “人工智能” 可能是两个 token也可能是一个取决于模型用的分词器。不同模型家族用的分词方案不一样同一个句子在不同模型里 token 数量可能差出百分之二三十。这件事为什么重要因为计费按 token 算上下文窗口按 token 算推理速度也按 token 算。你觉得自己只写了五百字实际可能是一千二百个 token。你觉得自己离上下文上限还有距离实际可能已经快撑满了。我见过一个团队做长文档摘要本地测试时用英文文档跑得好好的上线换成中文文档后频繁触发长度截断排查了半天才发现是中文 token 密度和英文差异太大同样的字数中文消耗的 token 多得多。更隐蔽的是tokenization 还会影响模型对内容的理解。有些特殊字符、罕见符号、混合语言片段在分词时会被切得支离破碎模型看到的和你以为它看到的完全不是一回事。比如你在 prompt 里写了一个复杂的 JSON 结构里面嵌套了好几层如果分词器把某些键名切成了奇怪的片段模型解析起来就容易出错。这不是模型笨是你喂给它的东西在第一步就已经变形了。实操建议在正式接入之前先用对应模型的分词工具跑一遍你的典型输入看看实际 token 数是多少看看有没有被切得很奇怪的片段。这个习惯能帮你避开很多莫名其妙的“模型不听话”问题。2.2 请求到达服务端之后批处理与调度的隐形等待你的 HTTP 请求到达推理服务端之后并不会立刻被送进模型。服务端要做的事情比你想象的多。首先是请求排队。推理服务通常不是来一个请求就立刻处理一个而是攒一批一起处理这叫批处理batching。批处理能大幅提升 GPU 利用率因为 GPU 最擅长的就是并行计算。但代价是如果你的请求恰好在一个批次刚满之后到达就得等下一批攒够。这个等待时间可能很短也可能很长取决于当前服务的负载情况。然后是显存分配。模型权重是常驻显存的但每次推理还需要额外的显存来存放中间计算结果。如果当前显存不够服务端要么等要么把请求排到后面。这就是为什么高峰期 API 响应会变慢——不是模型变慢了是排队和资源调度变慢了。还有一个容易被忽略的环节请求预处理。服务端收到你的请求后要解析 JSON、校验参数、拼接对话模板、应用系统提示词、处理特殊 token 等等。这些操作本身不慢但在高并发场景下会累积成可观的延迟。理解这一层的意义在于当你发现 API 响应时间波动很大时不要第一反应就是“模型不稳定”。先看看是不是服务端负载问题是不是你的请求恰好撞上了高峰期。很多所谓的“性能问题”其实是调度问题。2.3 模型真正开始计算自回归生成与 KV Cache终于到了模型计算这一步。大模型生成文本的方式是自回归的一次生成一个 token然后把新生成的 token 追加到输入后面再生成下一个如此循环。这意味着生成一百个 token 的回复模型要跑一百次前向计算。每次计算都要处理之前所有的 token。如果每次都从头算计算量会随长度平方增长根本跑不动。所以有了 KV Cache 这个关键技术。KV Cache 的思路是把之前计算过的注意力键值对缓存起来生成新 token 时只计算新 token 的注意力然后和缓存拼接。这样每次前向计算只需要处理一个新 token计算量从平方级降到了线性级。但 KV Cache 是有代价的。它占显存而且占得不少。上下文越长KV Cache 越大。这就是为什么长上下文推理又慢又贵——不只是输入 token 多还因为 KV Cache 把显存吃掉了能同时处理的请求数就少了。还有一个细节KV Cache 在流式输出时特别关键。你看到文字一个个蹦出来背后就是模型每生成一个 token 就立刻返回一个同时把新的 KV 对追加到缓存里。如果服务端不支持 KV Cache 复用流式输出的延迟会高得没法用。2.4 采样策略为什么同样的输入会有不同的输出模型计算出来的是一组概率分布表示下一个 token 可能是什么。从这组概率到最终选出的那个 token中间隔着一个采样策略。最常见的采样参数是 temperature 和 top_p。Temperature 控制概率分布的平滑程度温度越低高概率 token 的优势越大输出越确定温度越高低概率 token 也有机会被选中输出越多样。Top_p 则是另一种思路只从累积概率达到 p 的那部分 token 里采样把长尾直接砍掉。这两个参数同时设置时通常是先应用 temperature 调整分布再应用 top_p 截断。但具体实现顺序不同框架可能有差异这就是为什么有时候你调了参数感觉“没生效”——可能被另一个参数覆盖了。还有一个容易被忽略的参数是 top_k它直接限制只从概率最高的 k 个 token 里选。Top_k 和 top_p 的区别在于top_k 是固定数量top_p 是动态数量。有些场景下 top_k 更稳定有些场景下 top_p 更灵活。理解采样策略的意义在于你能解释为什么同一个 prompt 两次调用结果不一样能根据场景选择合适的参数组合而不是盲目地把 temperature 设成 0 就以为能获得确定性输出。实际上 temperature 设成 0 也不保证完全确定因为底层还有浮点数精度、并行计算顺序等不确定因素。3. 参数背后的工程含义那些文档里不会写清楚的取舍3.1 Temperature 与 Top_p 的配合逻辑不是调了就有用很多开发者对 temperature 的理解停留在“越高越有创意越低越稳定”。这个理解不算错但太粗糙了。Temperature 的本质是调整 logits 的缩放系数温度 T 会把原始 logits 除以 T然后做 softmax。T 小于 1 时分布变得更尖锐高概率 token 更高概率被选中T 大于 1 时分布变得更平坦低概率 token 也有机会。但 temperature 有一个问题它是对所有 token 一视同仁地缩放。如果模型本身对某个位置非常确定温度再高也很难让它选别的如果模型本身很犹豫温度稍微一调输出就天差地别。这就是为什么单纯调 temperature 有时候感觉“不跟手”。Top_p 解决的是另一个问题它直接砍掉长尾。不管 temperature 怎么调top_p 只从累积概率达到 p 的 token 集合里采样。这意味着那些概率极低的奇怪 token 永远不会被选中输出质量更稳定。实际使用中我的经验是需要确定性输出时temperature 设 0 到 0.3top_p 设 0.1 到 0.5需要多样性时temperature 设 0.7 到 1.0top_p 设 0.9 到 0.95。两个参数不要同时往极端调否则要么输出太死板要么输出太离谱。还有一个坑有些 API 文档说 temperature 和 top_p 二选一调整但实际实现里两个都生效。如果你只调了一个发现效果不对试试把另一个也调一下。3.2 流式输出的实现原理SSE 与 chunk 拼接流式输出是大模型应用里用户体验的关键。没有流式输出用户要盯着空白屏幕等好几秒有了流式输出文字一个个蹦出来感觉快得多。流式输出的底层通常是 Server-Sent EventsSSE。服务端和客户端建立一个长连接服务端每生成一个 token 就通过这个连接推送给客户端。客户端收到后立刻渲染同时继续等待下一个。但这里有几个工程细节容易出问题。第一SSE 推送的不是完整的 token而是 chunk。一个 chunk 可能包含一个 token也可能包含多个 token还可能把一个 token 拆成多个 chunk。客户端需要自己拼接这些 chunk拼错了就会出现乱码或断字。第二流式输出的结束标志不是连接关闭而是一个特殊的结束 chunk。如果你只监听连接关闭事件可能会在最后一个 chunk 还没处理完时就认为结束了。第三流式输出和错误处理要配合好。如果生成到一半出错了服务端可能直接断开连接客户端需要能识别这种情况并给出合理提示而不是一直转圈。我踩过的一个坑早期做流式输出时没有处理 chunk 边界结果中文输出经常出现半个字的情况。后来在客户端加了一个缓冲区确保每次渲染的都是完整的字符问题才解决。3.3 上下文窗口管理截断、摘要与滑动窗口上下文窗口是有限的但对话可以无限长。这两者之间的矛盾是所有对话类应用都要面对的问题。最简单的策略是截断超过窗口就丢掉最早的消息。但这样会丢失重要信息用户会发现机器人“失忆”了。稍微好一点的是滑动窗口保留最近 N 轮对话更早的丢掉。但这样还是会丢信息只是丢得慢一点。更聪明的做法是摘要把早期对话压缩成一段摘要放在系统提示词里。这样既保留了关键信息又节省了 token。但摘要本身也要消耗 token 来生成而且摘要质量不稳定可能把重要细节漏掉。还有一种做法是检索把历史对话存到向量数据库里每次根据当前问题检索相关片段拼进上下文。这样理论上可以支持无限长的对话历史但检索本身有延迟而且检索不准时会引入无关信息。实际项目中我通常组合使用这几种策略最近几轮对话保留原文更早的做摘要再早的存起来按需检索。具体比例根据场景调整客服场景可能更依赖检索创作场景可能更依赖摘要。3.4 系统提示词的真实作用不是“设定人设”那么简单系统提示词system prompt经常被当成“给模型设定人设”的工具。这没错但它的作用远不止于此。系统提示词是模型在生成每个 token 时都会看到的上下文的一部分。它会影响模型对整个对话的理解影响模型选择什么样的语气、什么样的知识、什么样的推理路径。一个好的系统提示词能显著提升输出质量一个差的系统提示词能让模型表现下降一个档次。我见过很多团队把系统提示词写得很随意几句话就完事。然后抱怨模型“不听话”。实际上系统提示词需要精心设计要明确任务边界要给出输出格式要求要说明什么该做什么不该做要提供必要的背景知识。这些内容不是“人设”是“工作说明书”。还有一个细节系统提示词的位置和格式会影响效果。有些模型对系统提示词的遵循度更高有些模型会把系统提示词和用户消息混在一起理解。了解你用的模型对系统提示词的处理方式能帮你写出更有效的提示词。4. 成本与性能的平衡从 token 计费到推理加速4.1 Token 计费的隐藏成本输入输出不对称大模型 API 的计费通常是输入 token 和输出 token 分开算的而且输出 token 往往比输入 token 贵。这个差价反映的是推理成本的不对称输入 token 可以并行处理输出 token 必须串行生成。这意味着什么意味着你的成本大头可能在输出上。一个应用如果输出很长即使输入很短成本也可能很高。反过来如果你能把输出压缩得简洁一些成本会明显下降。但压缩输出不能牺牲质量。我的经验是在系统提示词里明确要求“简洁回答”“不要重复问题”“不要解释显而易见的事情”能省下不少输出 token。另外对于结构化输出比如 JSON让模型直接输出目标格式而不是先输出一段解释再输出 JSON也能省 token。还有一个容易被忽略的成本重试。如果模型输出不符合要求你需要重新调用这就产生了额外成本。减少重试的关键是提高首次调用成功率把要求写清楚把格式定明确把边界条件说明白。4.2 批处理与并发什么时候该等什么时候该冲批处理能省钱因为很多 API 对批处理请求有折扣。但批处理意味着等待不适合实时交互场景。并发能提速但并发太高会触发限流反而更慢。而且并发请求之间可能互相影响比如共享的 KV Cache 被挤占。我的经验是实时交互场景用低并发加流式输出保证响应速度离线处理场景用批处理加高并发追求吞吐量。两者不要混在一起否则两头不讨好。还有一个技巧对于可以预测的请求提前发起。比如用户打开对话界面时你可以先发一个空的或者极短的请求预热连接等用户真正输入时延迟会低一些。4.3 缓存策略哪些请求可以复用哪些不能大模型推理的缓存分好几个层次。最底层是 KV Cache服务端自动管理开发者一般不用操心。上一层是语义缓存如果两个请求的语义相同或高度相似可以复用之前的输出。语义缓存的实现方式通常是把请求向量化在缓存里找相似度超过阈值的记录如果找到就直接返回缓存结果。这样能省下推理成本但风险是缓存结果可能不完全适用当前请求。我一般只在特定场景用语义缓存FAQ 类问答、固定格式的转换任务、对实时性要求不高的场景。对于需要结合上下文、需要个性化输出的场景语义缓存容易出问题。还有一个简单的缓存对完全相同的请求直接返回缓存结果。这个没有语义相似度的风险但命中率低只适合重复请求多的场景。4.4 模型选择不是越大越好也不是越小越省模型选择是成本与性能平衡的核心决策。大模型能力强但贵且慢小模型便宜快但能力有限。怎么选我的框架是先看任务复杂度。简单的分类、抽取、格式转换小模型足够复杂的推理、创作、多轮对话大模型更稳。再看延迟要求。实时交互场景小模型或中等模型更合适离线处理场景可以用大模型慢慢跑。最后看预算。预算充足就上大模型预算紧张就小模型加精心设计的提示词。还有一个策略是模型级联先用小模型处理如果小模型置信度低或者输出不符合要求再升级到大模型。这样大部分请求用小模型处理只有少数难题用大模型整体成本可控。但级联会增加系统复杂度而且小模型的判断不一定准。我一般只在请求量大、成本压力大的场景用级联小规模应用直接用中等模型更省心。5. 排查与调试当 API 返回的结果不符合预期时5.1 从日志开始记录什么才有用排查大模型问题时日志是第一手资料。但很多团队的日志记录得太少出问题了根本不知道当时发生了什么。我建议至少记录这些信息完整的请求参数包括所有采样参数、完整的输入 token 序列或者至少是 token 数量、完整的输出、响应时间、使用的模型版本。如果涉及多轮对话还要记录对话历史。但记录完整输入输出有隐私风险所以要做好脱敏。我的做法是生产环境记录 token 数量和哈希值不记录原文调试环境记录完整内容但设置访问权限和自动清理。还有一个技巧给每个请求打上唯一标识这样可以在日志里追踪一个请求的完整生命周期从客户端发出到服务端处理到返回结果。5.2 常见异常的分类与定位大模型 API 的异常大致分几类网络类超时、连接断开、服务端类限流、内部错误、模型类输出不符合预期、参数类参数不合法或冲突。网络类异常通常重试就能解决但要注意重试策略指数退避比固定间隔好加随机抖动比不加好。服务端类异常要看具体错误码限流就降速内部错误就重试。模型类异常最麻烦因为不是“错误”只是“不符合预期”需要仔细分析输入输出。参数类异常最好排查看文档对照就行。我遇到最多的是模型类异常表现五花八门输出格式不对、内容跑偏、重复啰嗦、突然截断。这类问题往往不是单一原因而是提示词、参数、上下文共同作用的结果。排查时需要控制变量先固定参数调整提示词再固定提示词调整参数。一步步缩小范围。5.3 提示词调试的工程化方法提示词调试不能靠感觉要有工程化的方法。第一步是建立评估集收集一批典型输入和期望输出作为调试的基准。没有评估集你改提示词就是盲人摸象改好了一个 case 可能弄坏了另外三个。第二步是自动化评估写脚本批量跑评估集自动对比输出和期望给出通过率。这样每次改提示词都能快速看到效果。第三步是版本管理提示词也要像代码一样版本管理每次修改记录改了什么、为什么改、效果如何。这样出问题了可以回滚也可以追溯哪个改动导致了效果下降。我见过很多团队提示词改来改去最后不知道哪个版本效果好就是因为没有版本管理。这个习惯建立起来之后提示词优化的效率会高很多。5.4 性能问题的排查路径性能问题通常表现为响应慢或吞吐低。排查路径可以从外到内先看网络延迟再看服务端排队时间再看模型推理时间最后看输出处理时间。网络延迟用 ping 或 traceroute 排查。服务端排队时间看 API 返回的响应头或者日志。模型推理时间看首 token 延迟和总生成时间。输出处理时间看客户端渲染和拼接逻辑。一个常见误区是只盯着模型推理时间忽略了其他环节。实际上在高并发场景下排队和网络往往是大头。我遇到过一次性能问题排查了半天发现是客户端拼接 chunk 的逻辑太低效每个 chunk 都触发一次重渲染导致 CPU 跑满。改成批量渲染后问题立刻消失。6. 从原理到实践几个能立刻用上的优化技巧6.1 减少 token 消耗的五个具体做法第一个做法是精简系统提示词。系统提示词在每个请求里都会重复发送如果写得太长累积成本很可观。把不必要的解释、示例、重复说明删掉只保留核心指令。第二个做法是压缩历史对话。多轮对话里早期轮次如果和当前问题无关可以摘要或者直接丢弃。不要把所有历史都塞进上下文。第三个做法是控制输出长度。在提示词里明确要求简洁设置 max_tokens 上限避免模型长篇大论。第四个做法是复用前缀。如果多个请求共享相同的前缀比如相同的系统提示词和少量示例可以利用前缀缓存来减少重复计算。有些 API 支持显式的前缀缓存有些是自动的。第五个做法是选择合适的模型。不是所有任务都需要大模型小模型能做的就用小模型省下的钱可以用在真正需要大模型的地方。6.2 提升输出稳定性的参数组合输出不稳定通常表现为同样的输入有时候输出好有时候输出差。提升稳定性的关键是降低采样随机性。我的推荐组合是temperature 设 0.1 到 0.3top_p 设 0.1 到 0.3同时设置 frequency_penalty 和 presence_penalty 为 0 或很小的值。这样输出基本确定但又不至于完全死板。如果还是不稳定检查一下是不是上下文里有冲突信息。模型在矛盾信息面前容易摇摆清理掉矛盾信息往往比调参数更有效。还有一个技巧在系统提示词里明确要求“直接回答不要犹豫不要输出多个版本”。这能减少模型输出“一方面……另一方面……”这种摇摆内容。6.3 流式输出的正确实现方式流式输出的实现要注意几个点。第一正确处理 chunk 边界确保每次渲染的都是完整字符。第二正确处理结束标志不要依赖连接关闭。第三正确处理错误生成中途出错要有合理提示。第四正确处理背压如果客户端渲染慢要能暂停接收避免内存堆积。我的实现通常是在客户端加一个缓冲区收到 chunk 先存起来然后按字符边界切分完整的字符立刻渲染不完整的留在缓冲区等下一个 chunk。结束标志收到后把缓冲区剩余内容渲染完然后关闭连接。错误处理方面我会在流式连接上监听错误事件一旦出错就停止渲染显示错误提示并记录日志。不要试图在流式过程中重试因为已经渲染的内容没法撤回重试会导致内容重复。6.4 成本监控与告警的搭建成本监控不是等到账单出来才看要实时监控。我的做法是每次 API 调用都记录 token 消耗和估算成本汇总到监控系统设置日预算和月预算告警。告警阈值要合理太低会频繁误报太高就失去意义。我一般设三个级别日预算的 50% 提醒80% 警告100% 严重告警。严重告警时自动降级到小模型或者暂停非关键请求。还有一个细节区分不同业务线的成本。如果多个功能共用同一个 API key成本混在一起很难归因。给每个业务线单独的 key 或者单独的标签能帮你快速定位成本异常的来源。监控数据还可以用来优化看看哪些请求消耗 token 最多哪些功能成本最高然后针对性地优化。很多时候优化一个高频低价值的功能比优化一个低频高价值的功能更能省钱。7. 一些踩过的坑和想明白的事7.1 关于“模型不稳定”的误解早期我总觉得模型“不稳定”同样的输入输出时好时坏。后来想明白了模型本身是确定的给定相同的输入和参数输出应该相同忽略浮点误差。不稳定的是我的输入——我以为是相同的输入实际上 tokenization 后可能不同上下文可能不同服务端状态可能不同。想明白这件事之后我不再抱怨模型而是仔细检查自己的输入。把输入固定下来把参数固定下来把上下文固定下来输出自然就稳定了。如果还不稳定那就是服务端的问题不是模型的问题。7.2 关于“提示词工程”的边界提示词工程有用但边界比很多人想象的要窄。它能引导模型的行为但不能让模型做它能力之外的事。你没法通过提示词让一个小模型拥有大模型的推理能力也没法通过提示词让模型知道它训练数据里没有的知识。提示词工程的核心是“对齐”让模型的输出和你的期望对齐。对齐的前提是模型有能力产生你期望的输出。如果模型没这个能力再好的提示词也没用。所以我的做法是先选对模型再写提示词。模型选错了提示词写得再好也是事倍功半。7.3 关于“成本优化”的优先级成本优化有很多手段换小模型、压缩输入、控制输出、缓存、批处理。但优先级很重要。我的优先级是先优化高频高成本的请求再优化低频高成本的请求最后优化高频低成本的请求。低频低成本的请求直接忽略优化它们的时间成本比省下的钱还多。还有一个原则不要为了省钱牺牲质量。如果优化导致输出质量下降用户流失的成本远大于省下的 API 费用。质量优先成本其次。7.4 关于“原理学习”的投入产出比学原理要花时间但回报是长期的。理解原理之后你排查问题更快优化更有方向和团队沟通更顺畅。而且原理是通用的换个模型、换个 API、换个框架原理还是那些原理。我的建议是不要一开始就啃论文从工程实践中的问题出发遇到不懂的再往下钻。比如你发现流式输出有延迟就去查 KV Cache 的原理你发现成本高就去查 tokenization 和计费的逻辑。这样学起来有目标记得也牢。8. 写给想从“会调”到“懂原理”的开发者从会调到懂原理中间隔着的不是智商是愿不愿意多问几个为什么。每次调用 API 的时候多想想这个请求到底发生了什么这个参数到底影响了什么这个结果到底是怎么产生的。想多了原理自然就通了。我自己的路径是先跑通再拆解再优化。跑通是第一步但不要停在第一步。拆解是把黑盒打开看看里面有什么。优化是在理解的基础上让系统跑得更好、更省、更稳。这个过程没有捷径但有方法。方法就是遇到问题不要绕过去追到底。追到底的过程中你会遇到很多不懂的概念一个个搞懂认知就上来了。大模型应用开发这个领域变化很快但底层原理变化很慢。把原理搞扎实上层怎么变你都能跟上。只会调 API 的人换个 API 就要重新学懂原理的人换个 API 只是换个调用方式。这个系列我会继续写下去下一篇想聊聊多轮对话的状态管理那是另一个被严重低估的工程问题。如果你也在做类似的事情欢迎交流。
阅读完成 · 觉得有帮助?