☰
大模型API调用全链路:密钥管理、流式输出与工程化落地
2026/10/27 4:23:32 网站建设 项目流程

调用大模型 API 接口这件事,乍看只是把一串 API Key 塞进请求头、往接口地址 POST 一段 JSON 就完事了,我最初也是这么想的,直到第一次把 demo 交给业务方跑,才在半小时内被四个问题按在地上摩擦:Key 写死在前端被打包进去了、超时没设导致页面转圈转到用户以为死机、429 一来整个功能全挂、月底账单比预算高了六倍。所以才有了这篇东西。它讲的不是“怎么点一下按钮生成密钥”,而是面向境内开发团队,把调用大模型 API 接口这件事从拿API Key到上线跑稳的全过程拆开:密钥怎么申请与管理、请求体里每个参数到底在干什么、流式输出怎么接、网关要做哪几件事、限流重试退避怎么算、账单怎么控、报错怎么排。刚接触的新手可以照着步骤跑通第一条请求,已经在维护线上服务的老手也可以直接跳到工程化和排查两节抄配置。需要提前说明的边界:本文只讨论在遵守平台服务条款与所在地相关规定的前提下,使用官方开放平台提供的合法服务,网络访问层面的操作不在讨论范围内,也不建议任何绕过服务条款的做法——技术上能跑通和商业上能长期跑,是两件完全不同的事。

1. 先把"调接口"这件事拆开看

1.1 一条请求从键盘到模型,中间经过了几道手

很多人对这条链路的想象是一条直线:我的代码 → 模型 → 结果。真实链路更像一条流水线,中间有七八个工位,任何一个工位堵住,你看到的现象都是"没有返回"。理解这条流水线,是后面所有排查工作的地基。

完整的路径大致是:你的客户端发起请求 → 到达你的后端服务 → 经过你自己搭的统一接入层(如果有)→ 出网到平台边缘节点 → 平台做身份鉴权 → 校验请求体格式与上下文长度 → 扣减配额 → 排队进推理集群 → 模型逐 token 生成 → 结果按你要求的形态(一次性返回或流式推送)回传 → 平台侧计量落库 → 你的服务收到并做后处理 → 客户端渲染。

这条链路上,只有"模型生成"这一段是慢的,其他环节都是毫秒级。这解释了一个非常实用的现象:如果你的请求 200ms 就报错了,那问题一定在鉴权、参数或配额上,跟模型没关系;如果请求挂了 60 秒才超时,那才是推理侧或网络侧的问题。我在排查时习惯先看响应耗时,一秒以内挂掉的直接去翻错误码,三十秒以上挂掉的第一时间去看超时配置和上下文长度。

另一个容易忽略的点是"计量"。平台计费发生在服务端完成生成之后,也就是说,你主动断开连接并不能省下已经生成的 token。有同事做过实验:流式输出跑了 300 字然后用户关掉页面,账单照样按 300 字计。所以真正的省钱动作发生在发请求之前——控制max_tokens、精简提示词、做缓存,而不是在中途掐断。

1.2 API Key 到底是什么,为什么它比密码更危险

大部分人把 API Key 当成"高级一点的密码",这是个很危险的类比。密码通常配合账号、二次验证、异地登录提醒,被盗了还有一层缓冲;而 API Key 在绝大多数平台上是Bearer 凭证——谁拿到这串字符,谁就完整地拥有了你账号在该 Key 权限范围内的全部能力,不需要任何额外验证,平台也没法区分"这是你本人在调"还是"这是别人拿着你的 Key 在调"。

更麻烦的是它的计费属性。密码泄露的损失是数据,Key 泄露的损失是持续流出的钱。我见过最典型的事故模式是:前端项目里为了"快速联调"把 Key 写进了环境变量文件,构建时被打进 bundle,上线后被人从静态资源里扒出来,然后就是一夜之间跑掉一个月的预算。这类事情的技术根因其实非常朴素——Key 出现在了一个不该出现的位置。

所以我在任何项目里都会先立三条规矩,不分项目大小:

  • Key 只允许出现在服务端进程的内存或服务端环境变量里,绝不进前端代码、绝不进 Git 仓库、绝不进日志。
  • 每个环境(本地开发、测试、预发、生产)用不同的 Key,出事时能精准吊销,不至于一刀切停掉全公司。
  • 每个 Key 绑定明确的用途和负责人,命名里写清项目与场景,比如proj-xxx-chat-prod-202503。

注意:哪怕只是给自己电脑上的脚本用,也不要把 Key 硬编码在.py文件里再同步到网盘或笔记软件。我习惯的做法是本地.env文件加.gitignore,以及一个开机时手动export的 shell 变量,多花十秒,省掉后面一堆麻烦。

1.3 三条接入路径的取舍,别一上来就选最复杂的

实际可选的接入形态基本就三种,我把它们的差异整理成一张表,你可以直接按自己的场景对号入座:

路径适合谁优点代价
官方开放平台直连个人开发者、早期验证、单一模型需求参数最全、新能力第一时间可用、文档最权威需要自己处理密钥安全、重试、限流
云厂商托管的模型服务已有云上基础设施的团队与既有账号体系、计费、审计打通,开票和合规流程顺模型版本更新可能滞后,参数有裁剪
自建统一接入网关多业务线、多模型并存的中大型团队密钥不出内网、可做配额分摊、可做多模型路由与降级前期投入大,需要人维护

我自己的经验路径是:验证阶段一定走官方直连,用最原始的方式把参数摸熟;一旦有两个以上业务方要用,立刻上网关,哪怕只是一个 200 行的服务。中间那层网关的价值不在于"转发",而在于它是唯一知道所有 Key 的地方,业务方拿到的是一张内部签发的令牌,永远看不到上游密钥。这个隔离做不做,直接决定了你未来某天要不要半夜起来换 Key 并通知五个团队。

如果模型选型还没定,还有一个折中做法:在网关里做一层协议适配,把各家略有差异的请求体归一成一套内部接口,业务代码只面向内部接口编程。这样将来换模型供应商时,改动量集中在网关的适配层,业务侧一行不动。我在一个项目里就是因为早期偷懒没做这层,后来换模型时改了十几个调用点,每个的入参结构都不一样,那两天改得相当难受。

2. 密钥申请与账号体系:从注册到拿到第一把 Key

2.1 注册之前,先把三件事想清楚

很多人注册完才发现卡在某个环节,来回折腾。这三件事提前准备好,能省掉大量返工。

第一是用途说明。主流平台在创建应用时通常要求填写用途描述,写得含糊(比如"测试")有可能触发额外审核,写得具体("为客服工单系统提供自动摘要与分类能力")通常过得更顺。这不是形式主义,平台需要判断你的调用场景是否属于允许范围。

第二是支付方式。免费额度能撑起验证,但撑不起上线。建议在验证阶段就跑通一次最小额度的充值流程,把发票信息、付款主体、预算告警一起配好。我吃过一次亏:功能上线当天才发现财务流程需要提前走审批,结果免费额度耗尽后服务直接不可用,业务方在群里问了一下午。

第三是配额与限流的初始值。新账号的默认并发和日调用上限通常比较保守,如果你的场景是批量处理(比如一次性给几千条历史工单打标签),需要提前申请提额,提额一般需要说明预估调用量和业务背景,审批需要时间。这件事一定要在上线前一周做,不要等到跑批那天。

2.2 控制台里那四个必须弄懂的概念

进去之后别急着点"创建密钥",先把这四个东西的分层关系搞清楚,能避免后面 80% 的混乱。

  • 组织/账号:计费和结算的主体,一个组织下可以有多个项目。
  • 项目/应用:逻辑隔离单位,配额、用量统计、密钥一般挂在项目下面。
  • 密钥(API Key):真正用于鉴权的凭证,一个项目下可以有多把,用于区分不同环境或不同调用方。
  • 配额(Quota / Rate Limit):分两层,一层是 RPM/TPM(每分钟请求数/每分钟 token 数),一层是账单额度上限。前者决定你会不会被 429,后者决定你会不会被停服。

这几层的意义在于故障隔离。举个例子,市场部的活动页要接一个文案生成功能,如果它和你核心业务共用一把 Key,活动一爆量,核心业务就被 429 打死。正确做法是给它单独开一个项目、单独一把 Key、单独设一个额度上限,它怎么疯都撞不到你。

2.3 创建第一把 Key 的实操步骤与命名规范

流程本身各家平台大同小异,我按通用步骤写,你对照自己控制台的按钮找即可:

  1. 登录开发者控制台,进入"API 密钥"或"应用管理"页面。
  2. 新建应用/项目,填写名称与用途描述,选择要开通的模型能力。
  3. 在项目下点击"创建密钥",部分平台会让你选择权限范围(只读、仅推理、含管理权限),生产环境一定只给推理权限。
  4. 密钥只在创建时完整展示一次,之后只能看到前后几位。这时候立刻复制到一个安全的地方,别指望"等会儿再复制",页面一刷新就没了,只能重新创建。
  5. 顺手把用量告警和账单上限设置好,阈值建议设在预算的 60% 和 90% 两档。

命名这件事值得单独说。我现在的命名格式是{项目}-{场景}-{环境}-{年月},例如kb-assistant-prod-202503。好处是三个月后你翻控制台,一眼就能看出哪把 Key 是可以删的历史遗留,哪把是正在跑的。曾经有个项目我图省事建了五把名字叫test、test2、new的 Key,半年后完全不敢删,因为不知道哪把还有调用方——最后只能靠翻网关日志一个个比对,花了大半天。

2.4 密钥的生命周期:轮换、吊销、分环境

密钥不是建完就一劳永逸的东西。我建议给每把生产 Key 设一个最长 90 天的轮换周期,流程是:新建一把新 Key → 在网关里配置双 Key 并存 → 观察一周确认新 Key 调用正常 → 把旧 Key 下线 → 一周后吊销。这个"双活过渡"的设计可以做到零停机轮换,成本几乎为零。

吊销则是应急手段,必须提前演练。你要能在五分钟内回答三个问题:这把 Key 在哪几个服务里被引用?吊销后哪些功能会挂?备用 Key 在哪、怎么切?如果答不上来,说明你的密钥管理还没到位。我的做法是在网关的配置里把 Key 做成列表,支持热加载,出问题时改一行配置、发个信号,三秒生效,不用重启服务。

注意:吊销是立即生效且不可逆的。我见过有人在排查 401 时手滑点了吊销,结果把还在跑批的任务打断。所以在做任何吊销操作前,先确认调用方的真实身份,宁可多查五分钟。

3. 第一次跑通调用:从 curl 到 SDK

3.1 用 curl 先验证鉴权,别一上来就写代码

我最推荐的第一步永远是 curl。原因很简单:它把"代码层面的问题"从链路里剔除掉了,剩下的只有网络、鉴权、参数三件事。如果 curl 通了而代码不通,问题一定在你的代码或环境变量里,排查范围瞬间缩小一半。

curl -X POST "https://api.example-llm.com/v1/chat/completions" \ -H "Authorization: Bearer $LLM_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "model": "chat-standard", "messages": [ {"role": "system", "content": "你是一名严谨的技术文档编辑。"}, {"role": "user", "content": "把下面这段日志归类:ERROR db connection timeout after 30s"} ], "temperature": 0.2, "max_tokens": 200 }'

几个实操细节。第一,不要把 Key 直接写在命令行里,shell 历史会把它记下来,用环境变量的方式引用(上面示例用的是$LLM_API_KEY)。第二,加-i可以看到响应头,里面通常有x-request-id,这个 ID 在提交工单时是必填项,也是你唯一能对上平台侧日志的线索,建议每次报错都把它记下来。第三,如果只是想测连通性,可以先请求一个"列出可用模型"的接口,成本为零,成功即说明鉴权和网络都没问题。

我习惯在 curl 里加一个-w参数看耗时:

curl -o /dev/null -s -w "status=%{http_code} total=%{time_total}s\n" \ -X POST "https://api.example-llm.com/v1/chat/completions" \ -H "Authorization: Bearer $LLM_API_KEY" \ -H "Content-Type: application/json" \ -d '{"model":"chat-standard","messages":[{"role":"user","content":"hi"}],"max_tokens":10}'

这样一眼就能看出是"快失败"还是"慢失败",前面讲过的判断法在这里就派上用场了。

3.2 请求体里每个参数到底在干什么

这一段是很多人跳过、然后反复踩坑的地方。我按重要性排序讲。

messages是核心。它是一个数组,每个元素有role和content。system用来定角色和约束("只输出 JSON,不要解释"),user是用户输入,assistant是历史回复。多轮对话的本质就是把历史消息全部带上重发一遍——模型本身是无状态的,它不记得上一轮说了什么。这也解释了为什么多轮对话越聊越贵:上下文在累积。

max_tokens是最该设的参数。它限制输出的最大长度,直接影响最坏情况下的花费和响应时间。不设或者设得过大,一旦模型开始"发散",你能收到几千字的废话和一张昂贵的账单。我的经验值:分类/抽取类任务设 100~300,摘要类设 500~800,长文生成设 2000 以上并配合超时。

计算方式很简单:预估花费 = 输入 token 数 × 输入单价 + 输出 token 数 × 输出单价。假设某平台输入单价 0.000002 元/token、输出单价 0.000006 元/token(示例数值,实际以你平台的价目表为准),一次请求输入 1200 token、输出 800 token,单次成本就是 1200×0.000002 + 800×0.000006 = 0.0024 + 0.0048 = 0.0072 元。如果你的日调用量是 1 万次,日成本就是 72 元,月成本约 2160 元。这个算式一定要在需求评审阶段就摆到桌面上,因为它往往会改变产品设计——比如把"每次都生成"改成"缓存命中优先"。

temperature和top_p建议只调一个。这两个都是控制随机性的,同时调容易互相干扰。分类、抽取、代码生成这类要确定性的任务,用temperature=0~0.3;创意文案用0.7~1.0。我遇到过线上偶发"同一输入返回不同分类结果"的问题,最后发现是 temperature 设了 0.9,模型在边界样本上左右摇摆。

stream决定返回形态。设为 true 时服务端会边生成边推,用户体验好很多,但客户端处理逻辑要复杂一些,下一节细讲。

response_format或类似的 JSON 约束参数,在做结构化抽取时非常好用,能显著降低"模型返回了一段解释而不是 JSON"的翻车率。但即使开启,也要在代码里做容错解析——我至今没有见过哪个系统能完全免疫这个问题。

3.3 流式输出的接入要点,坑比你想的多

流式返回用的是 SSE(Server-Sent Events)风格的文本流,服务端持续推送形如data: {...}的块,最后以一个data: [DONE]之类的结束标记收尾。看起来简单,实际有几个必踩的坑:

第一个是"粘包/半包"。网络传输不会保证每次读到的都是一个完整的 SSE 事件,你可能读到半个 JSON。正确做法是维护一个缓冲区,按换行符切分,只有当一行以data:开头且能被完整解析时才处理,剩下的部分留在缓冲区等下一批数据。

第二个是"结束标记不等于连接关闭"。有些实现收到结束标记后连接还在挂着,如果你用同步阻塞的读法会一直等下去。必须显式判断结束标记并主动关闭。

第三个是"错误也可能以流的形式出现"。请求已经返回 200 开始推流了,中途模型侧出错,错误信息会作为流里的一个事件推过来。如果你的代码只处理正常增量,这个错误会被静默吞掉,用户看到的是"输出到一半停住了"。

我给的一个最小可用处理骨架(Python,通用伪结构,具体字段名按平台文档调整):

buffer = "" for chunk in response.iter_lines(decode_unicode=True): if not chunk: continue buffer += chunk + "\n" while "\n" in buffer: line, buffer = buffer.split("\n", 1) line = line.strip() if not line.startswith("data:"): continue payload = line[5:].strip() if payload in ("[DONE]", "DONE"): return try: obj = json.loads(payload) except json.JSONDecodeError: continue # 不完整的块,等下一批 delta = obj.get("choices", [{}])[0].get("delta", {}).get("content") if delta: yield delta

第四个是前端体验。流式输出必须配一个"停止生成"按钮,用户随时能中断。中断时要真的把后端连接关掉,否则你的服务会一直挂着读完整个流,白白占用连接和并发额度。

3.4 多语言 SDK 与环境变量配置

直接用官方或社区 SDK 能省掉不少样板代码,但有两个纪律必须守。

纪律一:SDK 客户端的初始化只做一次。我见过在每次请求里 new 一个客户端的写法,结果连接池完全失效,高并发下大量时间花在建连上。正确做法是在应用启动时创建一个全局客户端,复用连接。

纪律二:所有配置从环境变量读,不从代码读。至少四项:LLM_API_KEY、LLM_BASE_URL、LLM_MODEL、LLM_TIMEOUT。把 base_url 和 model 也做成可配的,好处是切换模型或切换接入点(比如从直连切到网关)时不用改代码,改配置重启就行。这个习惯在后来我迁移供应商时救过一次急——业务代码零改动。

import os, httpx client = httpx.Client( base_url=os.environ["LLM_BASE_URL"], headers={"Authorization": f"Bearer {os.environ['LLM_API_KEY']}"}, timeout=httpx.Timeout(connect=5.0, read=60.0, write=10.0, pool=5.0), )

注意超时是分段的,connect短一点(5 秒足够建连,建不上说明网络有问题),read长一点(给模型生成留时间)。千万不要用一个笼统的timeout=30,那会导致长文本生成永远被掐断。分四段配置是我现在所有项目的默认写法。

4. 工程化落地:把 demo 变成能上线的服务

4.1 统一接入层到底要做哪五件事

网关这个词听起来很重,其实一个两三百行的服务就能覆盖 90% 的需求。它要做的就五件事,我按优先级排:

第一,藏住密钥。业务方调用网关时用的是内部令牌,网关在转发时替换成真实的上游 Key。这是最核心的价值。

第二,统一错误语义。上游返回 429、401、500、超时,网关各自映射成内部统一错误码,业务方只需要处理一套。没有这一层,每个业务方都要自己写一遍错误处理,而且一定写得五花八门。

第三,统一埋点。每次调用记录:调用方、模型、输入输出 token 数、耗时、是否成功、request-id。这份日志是后面做成本分摊和容量规划的唯一依据。我强烈建议从一开始就把"调用方"字段作为必填,否则后面算账时会发现一堆无主流量。

第四,做重试和限流。见后两节。

第五,做模型路由与降级。主模型超时或不可用时,可以按配置降级到备用模型,或者直接返回一个降级提示。这在多模型并存的架构里非常有用,同时也让 A/B 测试变得容易——按比例分流即可。

一个容易被低估的细节:网关不要把整个响应缓冲完再返回。如果业务方要的是流式输出,网关必须透传流,中间不能攒。我见过为了"方便记录 token 数"而把流读完再转发的实现,结果首字延迟从 300ms 变成了 8 秒,体验直接崩了。记录 token 数应该从响应尾部的用量字段读,不需要缓存正文。

4.2 超时、重试与退避,参数要算出来而不是拍出来

超时配置有个基本原则:外层超时必须大于内层超时之和。假设单次请求超时设 20 秒,最多重试 2 次,那业务方调用网关的超时至少要设 70 秒(20×3 + 缓冲),否则会出现"网关还在重试,业务方已经超时放弃"的错位,白白消耗配额。

重试策略的关键是只重试该重试的。我用的规则是:

错误类型是否重试说明
连接超时 / 读超时重试网络抖动或模型排队,重试大概率能成
429 限流重试,但必须退避立即重试只会加剧拥塞
500 / 502 / 503重试服务端临时故障
400 参数错误不重试重试一万次也是同样的错误
401 / 403 鉴权失败不重试需要人工介入
内容被安全策略拦截不重试换个问法才有意义

退避要带随机抖动,避免大量请求在同一时刻齐刷刷重试形成"惊群"。一个实用的参数组合是:基础延迟 500ms,指数因子 2,最大重试 3 次,每次加 0~30% 的随机抖动。这样三次重试的等待时间大致是 0.5s、1s、2s,加上抖动,总等待在 3.5~4.5 秒之间,配合 20 秒的单次超时,整体控制在 70 秒以内可接受。

关于重试还有个必须注意的点:非幂等场景要小心。如果一次请求已经生成了一部分内容(比如流式输出到一半断了),重试会导致用户看到重复内容。我的处理方式是:流式请求一旦开始推流就不重试,只做"重新生成"的用户级操作;非流式请求可以放心重试。

4.3 限流与并发:算清楚你需要多少

并发需求很容易被低估。计算方式其实很简单:

所需并发数 ≈ 峰值 QPS × 平均响应耗时(秒)

举例,如果你的场景是峰值每分钟 300 次调用(即 5 QPS),平均响应耗时 4 秒,那所需并发大约是 5 × 4 = 20。但如果你的流量是有尖峰的(比如整点推送、上班打卡前后),峰值 QPS 可能是均值的 5 倍,那就要按 25 QPS 算,所需并发 100。这个数字直接决定你要向平台申请多少配额。

反过来说,你也要给自己加保护。我通常会在网关做令牌桶限流,按调用方分配额度,超出直接返回"稍后重试",而不是放进去排队。排队的危害在于:队列一长,所有请求的耗时都变成"排队时间 + 推理时间",用户端看到的是全面变慢,而不是部分失败。快速失败比慢慢全挂要好得多。

还有个细节:并发的计量单位各家不同,有的按同时在飞的请求数,有的按 RPM/TPM。TPM 这个指标在长文本场景下比 RPM 更容易触发——你每分钟只发了 20 个请求,但每个输入 3 万 token,TPM 早就爆了。所以做容量规划时,RPM 和 TPM 两个都要算。

4.4 成本控制:三个立刻能生效的动作

动作一:精准控制max_tokens。这是最直接的杠杆。把"摘要"任务的 max_tokens 从 2000 降到 600,如果输出真的不需要那么长,成本直接砍掉一多半,响应也更快。

动作二:做提示词缓存。如果你的场景里 system prompt 很长且固定(比如几百字的角色设定加格式要求),把它放在消息数组的最前面并保持完全一致,很多平台对相同前缀有缓存机制,能显著降低输入侧成本。这个优化在客服、分类这类高频场景里效果非常明显。

动作三:加一层结果缓存。对于"同样输入返回同样结果可接受"的场景(比如常见问题的标准答复、固定错误码的解释),用输入内容的哈希做键缓存 24 小时,命中率往往能到 20%~40%。我在一个工单摘要功能上做过统计,一周内重复或高度相似的工单占了三分之一,加了缓存后成本降了约 28%。

同时设两道保险:账单告警(预算的 60%、90% 各一档,发到群里而不是只发邮件,邮件没人看)和硬上限(超过就停,而不是继续跑)。硬上限一定要设,宁可服务不可用也不能出现失控账单——前者是事故,后者是事故加财务问题。

4.5 日志与可观测性:出问题时你只有这些线索

要记录的字段我列一个最小集合:时间戳、调用方标识、模型名、输入 token、输出 token、总耗时、首字耗时(流式)、HTTP 状态码、平台返回的 request-id、错误码、错误摘要。

其中首字耗时这个指标很有价值,很多问题它比总耗时更早暴露。用户感觉"慢"往往是因为首字慢,而不是生成慢。首字耗时突然从 400ms 涨到 3 秒,通常意味着平台侧排队严重或者你的请求体突然变大(比如上下文没裁剪)。

注意不要记录请求和响应的完整正文,尤其不要记到日志平台里长期留存。这些内容可能包含用户隐私或业务敏感信息。我的做法是只记长度、哈希和必要的元数据,需要排查具体内容时,靠 request-id 去平台的调用记录里查,并且设置保留期限。

监控面板我一般做四块:调用量趋势、成功率、耗时分布(P50/P95/P99)、成本日趋势。这四张图放在一屏上,绝大多数异常都能在十秒内定位到方向。

5. 常见报错与排查实录

5.1 鉴权类:401 和 403 的区别

401 表示"我不知道你是谁",通常是这几种情况:Key 拼错了(前后多了空格、复制时漏了字符)、环境变量没加载(本地跑得好,部署后挂,十有八九是这个)、Key 已被吊销、请求头格式不对(少写了Bearer前缀或者写成小写)。

403 表示"我知道你是谁,但你不能干这个",常见于:Key 的权限范围不含目标接口、模型未开通、所在地区或账号状态受限、请求的组织 ID 与 Key 不匹配。遇到 403 先去看控制台里这个项目开通了哪些模型,别急着怀疑代码。

排查顺序我固定为:先用 curl 复现 → 确认 Key 是否有效(换个最简单的接口试) → 检查请求头拼写 → 检查环境变量是否正确注入(在容器里env | grep LLM看一眼)→ 最后才怀疑平台侧。

5.2 参数与上下文长度类错误

400里最常见的两条是"参数格式错误"和"上下文长度超限"。前者多半是 JSON 结构有问题,比如messages传成了字符串、temperature传了超出范围的值。后者的表述通常是"maximum context length exceeded"。

上下文超限有个隐蔽的来源:多轮对话累积。用户在客服窗口聊了三十轮,你还把全部历史都带上,必然爆。解决方式是做滑动窗口:只保留最近 N 轮,加上一条固定的 system 消息,必要时把更早的内容压成一段摘要。我一般用"最近 6 轮原文 + 更早内容的 200 字摘要"这个组合,实测在大多数客服场景够用,token 量能压到原来的四分之一。

还有一个坑是"中文与 token 的比例"。中文一个汉字通常对应 1~2 个 token(不同分词器差异较大),做长度预估时按"字符数 × 1.5"估比较保险,别按"字符数 = token 数"算,会低估一半。英文相对接近 4 字符 1 token。写长度校验的时候把这两个系数写进代码注释里,省得后人再踩。

5.3 限流与超时:429 的正确处理姿势

429 是高频问题。首先要区分两种:短时突发和配额真的不够。前者退避重试就能解决,后者再怎么重试也没用,必须提额或者削峰。

判断方法很简单:看 429 的频率。偶尔出现、退避后成功,是突发;持续出现、重试也失败,是配额不足。我在网关里加了个计数器,429 在一分钟内超过 10 次就发告警,因为这时候基本可以确定不是抖动。

削峰的手段有几个:把非实时任务(批量打标、历史数据清洗)挪到低峰时段跑,用队列串行化;对用户侧做请求合并(多个相似请求合成一次);给功能加节流(用户连续点击只发一次)。这三个做得好的话,峰值能压掉一半以上。

超时的情况更复杂,要分"建连超时"和"读超时"。建连超时基本是网络层问题,检查出口、DNS、目标地址是否可达;读超时说明连上了但模型迟迟不返回,此时要看请求的输入长度(输入过长会导致首字延迟显著上升)和平台的负载状况。我遇到过一次大面积读超时,最后定位到是某个业务把 3 万字的文档整篇丢进去做摘要,输入侧处理就耗掉了大部分时间预算。后来加了"超过 8000 token 先分段再摘要"的逻辑,问题消失。

5.4 "软故障":接口没报错但结果不能用

这类问题最难排查,因为没有错误码。典型表现:返回空内容、返回了一段解释而不是要求的 JSON、返回了重复内容、返回的语言不对。

返回空内容通常是max_tokens设得太小,或者触发了内容策略被静默截断。先把 max_tokens 调大试试,能出内容就说明是这个原因。

不返回 JSON的应对方式是双管齐下:一方面在提示词里给明确的格式示例(给一个完整的 JSON 样例比写十句"请输出 JSON"有效得多),另一方面在代码里做"提取第一个{到最后一个}之间的内容"的容错解析,再加一层 JSON 校验,失败则重试一次。我给所有结构化抽取接口都加了这个兜底,线上失败率从 3% 降到 0.2%。

重复内容往往是 temperature 太低加上提示词里存在重复模式导致的,适当提高温度或者调整提示词能缓解。

5.5 排查速查表

现象最可能的原因第一步动作
100ms 内返回 401Key 未注入或拼写错误在容器内打印环境变量确认
100ms 内返回 403模型未开通 / 权限不足查控制台该项目的模型权限
400 提示上下文超限多轮历史累积或输入过长打印 messages 的总字符数,做窗口裁剪
429 偶发瞬时突发开退避重试,观察是否恢复
429 持续配额不足提额 + 队列削峰
30s+ 读超时输入过长或平台排队缩短输入,检查首字耗时曲线
返回空字符串max_tokens 过小调大后重试对比
输出不是 JSON提示词约束不足加格式示例 + 代码兜底解析
流式中途停住未处理流内错误事件加错误事件监听与用户提示
成本异常上涨某业务调用量突增或缓存失效按调用方维度看趋势,定位来源

心得:每次排查完,我都会把结论补进这张表里。半年下来它会变成团队最有价值的文档之一,因为新问题的第一次出现往往是老问题的变种。

6. 合规与安全的几条硬线

6.1 数据合规:发出去之前先想三秒

把数据发到外部服务这件事,必须有一套明确的规则,不能靠每个人自觉。我参与的团队里通用的三条是:不发送可识别个人身份的信息(姓名、证件号、联系方式、住址),确需处理时先在本地做脱敏替换;不发送未公开的商业信息(合同条款、财务数据、未发布的产品规划);不发送内部系统凭证(数据库地址、内部域名、密钥)。这三条写在代码评审清单里,比写在规范文档里有效得多。

技术上可以做一层出站过滤器,用正则匹配常见敏感模式(手机号、证件号格式、邮箱、内网 IP),命中则打码或拦截并记录。这东西写起来不到一百行,但要配合误报处理——比如订单号恰好符合手机号格式被打码,会导致功能异常。我的做法是先全量记录命中情况但只告警不拦截,跑一周看误报率,再决定开启拦截。

另外,用户侧的告知也要做到位。在功能入口明确说明"本功能由智能模型提供支持,请勿输入敏感信息",这句话看着像免责声明,实际能显著降低误输入率。

6.2 密钥泄露的应急处置

万一发现密钥可能泄露(比如误提交到了公开仓库、出现在前端产物里、被同事贴到了聊天群),按这个顺序处理,别犹豫:

  1. 立刻吊销,不要先"观察一下"。这时候每一分钟都在产生可能的费用。
  2. 创建新 Key,更新网关配置,确认服务恢复。
  3. 拉取泄露时间段的用量记录,看有没有异常调用量和异常调用来源。
  4. 复盘:它是怎么出去的?是代码里硬编码、构建产物带出、还是日志打印?堵住这个具体路径,而不是笼统地说"以后注意"。

关于第 4 步我想多说一句。我见过的泄露案例里,排名第一的原因是"本地调试时为了方便临时写死,后来忘了删",第二名是"把 Key 打印在错误日志里方便排查"。第二个尤其隐蔽,因为日志系统往往有多份副本、保留期很长、访问权限很宽。所以我在代码评审时会专门搜print、log、console.log附近有没有密钥变量,这个习惯救过至少两次。


最后分享几个我一直在用的小技巧。第一,在项目根目录放一个.env.example,把所有需要的变量名列出来但值留空,新人克隆下来就知道要配什么,不用问人。第二,给网关加一个/health接口,返回"当前配置了哪几把 Key、分别指向哪个模型",但不返回 Key 本身,排查时一眼就能确认配置有没有生效。第三,把本文第 5 节那张速查表打印出来贴在显示器边上,前两个月你会频繁用到它,之后就长在脑子里了。第四,也是我最想强调的一点:先跑通一次最朴素的 curl,再谈架构。我见过太多团队一上手就搭网关、上队列、做多模型路由,结果连一次成功的请求都还没发出去,最后卡在某个参数名拼错上折腾两天。链路是一段一段打通的,别跳步。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询