OpenAI API 生产级集成:密钥管理、错误处理与响应解析全链路
1. 这不是“调用API”,而是重建一次可靠的服务连接
很多人点开这篇教程,心里想的是:“复制粘贴几行代码,填个密钥,跑起来就完事了。”结果一执行,终端里刷出一串红色报错:401 Unauthorized、402 Insufficient balance、model at capacity、socket connection closed unexpectedly……然后卡住,再然后放弃。我见过太多人把“调用 ChatGPT API”当成一个纯技术动作——输入 token,调用openai.ChatCompletion.create(),等着返回 JSON。但现实是:它本质上是一次跨公网、带状态、受配额约束、需容错重试、要适配模型演进的生产级服务集成。你填进去的不是密钥,而是一张动态失效的电子通行证;你发出去的不是请求,而是一封可能被限流、被截断、被降级处理的信件。
这背后涉及三个常被忽略的底层事实:第一,OpenAI 的 API 网关不是静态路由,它会根据实时负载、用户等级、模型热度动态调度后端实例,同一请求在毫秒级内可能落到不同集群,响应延迟和成功率天然波动;第二,gpt-3.5-turbo和gpt-4o不是两个“函数名”,而是两套完全不同的推理栈——前者走轻量级蒸馏模型+缓存加速路径,后者走全参数大模型+多模态协同路径,对上下文长度、token 计费粒度、流式响应结构的处理逻辑完全不同;第三,“免费使用”“免登录镜像”等热词背后,是大量非官方中转代理服务,它们普遍缺乏请求队列管理、无 token 预校验、不透传原始错误码,把429 Too Many Requests包装成500 Internal Error,把403 Forbidden伪装成401 Unauthorized,让开发者在错误日志里原地打转。
所以本篇不叫“Python 调用 ChatGPT API 入门”,而叫“完整教程”。这个“完整”,指的是从环境初始化、密钥安全管控、请求构造逻辑、错误分类捕获、重试策略设计、响应解析鲁棒性,到本地缓存与日志审计的全链路闭环。我会用真实调试过程中的截图级细节告诉你:为什么temperature=0.7在某些场景下比0.0更稳定;为什么max_tokens设为2048可能导致context window limit报错,而设为2000却能通过;为什么你复制的“完整代码”在同事电脑上跑不通——问题不在 Python 版本,而在系统 DNS 缓存污染。这不是教你怎么写 hello world,而是带你亲手搭一座能扛住流量、经得起压测、查得出问题的桥。
2. 密钥不是字符串,是需要生命周期管理的敏感凭证
绝大多数失败始于第一步:密钥处理。新手常犯的错误不是“填错了”,而是“填得太直白”。
2.1 OpenAI 密钥的真实结构与风险面
OpenAI 的 API Key 格式为sk-开头、32 位小写字母与数字组合(如sk-prod-abc123def456ghij789klmn012opqr),它本质是一个Bearer Token,具备以下关键属性:
- 无状态性:服务端不存储密钥明文,只校验其签名有效性与绑定账户权限;
- 高权限性:单个密钥默认拥有该账户下所有已启用模型的调用权,且可创建/删除 fine-tune 作业;
- 无回收机制:一旦泄露,无法“冻结”或“临时禁用”,只能立即删除并生成新密钥;
- 无作用域限制:目前不支持按模型、按 endpoint、按 IP 白名单做细粒度授权(对比 GitHub Personal Access Token 的 scopes 机制)。
这意味着:把密钥硬编码在.py文件里,等于把家门钥匙焊死在防盗门上;写进requirements.txt,等于把钥匙复印件塞进快递包裹;存在 Git 历史里,等于把钥匙照片发到朋友圈还带定位。
提示:我曾协助一个团队排查持续三天的
401错误。最终发现是某成员在调试时,将密钥连同print(os.environ.get("OPENAI_API_KEY"))一起提交到了私有仓库。虽然仓库设为 private,但 CI 流水线配置了secrets.OPENAI_API_KEY,导致测试环境读取的是空值——而开发机本地.env文件里密钥早已过期。错误日志显示401,实际根源是密钥轮换未同步。
2.2 安全加载密钥的三级防护实践
正确的做法是建立三层隔离:
第一层:环境变量隔离(开发机)
不依赖 IDE 自动注入,手动创建.env文件(务必加入.gitignore):
# .env OPENAI_API_KEY=sk-prod-abc123def456ghij789klmn012opqr OPENAI_BASE_URL=https://api.openai.com/v1 # 可选,用于指向中转代理使用python-dotenv加载(安装:pip install python-dotenv):
from dotenv import load_dotenv import os # 显式指定路径,避免意外加载其他目录下的 .env load_dotenv(dotenv_path=".env") api_key = os.getenv("OPENAI_API_KEY") if not api_key: raise ValueError("OPENAI_API_KEY not found in environment variables")第二层:运行时校验(防空值/格式错误)
在初始化客户端前,增加基础校验:
import re def validate_api_key(key: str) -> bool: if not key or not isinstance(key, str): return False # 匹配 sk- 开头 + 至少 32 位字符 return bool(re.match(r"^sk-[a-zA-Z0-9]{32,}$", key)) if not validate_api_key(api_key): raise ValueError("Invalid OpenAI API key format")第三层:生产环境密钥托管(K8s/Serverless)
在云环境部署时,绝不用.env。以 AWS Lambda 为例:
- 将密钥存入 AWS Secrets Manager,设置访问策略;
- Lambda 执行角色附加
secretsmanager:GetSecretValue权限; - 代码中通过
boto3动态获取(绝不缓存到全局变量):
import boto3 import json def get_openai_key(): client = boto3.client('secretsmanager', region_name='us-east-1') response = client.get_secret_value(SecretId='prod/openai/api-key') return json.loads(response['SecretString'])['key']注意:本地开发用
.env,CI/CD 流水线用平台 Secret 注入(如 GitHub Actions 的secrets.OPENAI_API_KEY),生产环境用云服务商密钥管理服务。三者路径严格分离,这是底线。
2.3 密钥轮换的实操节奏与验证清单
密钥不是“一次生成,永久有效”。建议强制轮换周期:
- 个人开发者:每 90 天轮换一次;
- 团队项目:每次新成员入职/离职后立即轮换;
- 生产服务:上线前、重大版本更新后、安全审计前必须轮换。
轮换后,执行四步验证:
- 本地测试:用最小请求(
model="gpt-3.5-turbo", messages=[{"role":"user","content":"hi"}])确认200 OK; - 错误码触发:故意传入错误 model 名(如
"gpt-3.5-turbo-invalid"),验证是否返回404而非401(证明密钥有效,错误来自模型名); - 配额检查:调用
https://api.openai.com/v1/dashboard/billing/subscription(需额外权限),确认hard_limit_usd与account_name字段可读; - 日志回溯:检查最近 24 小时应用日志,确认无
AuthenticationError相关报错。
3. 请求构造不是填参数,而是设计一次语义精准的对话契约
很多“完整代码”示例只展示最简调用:
response = openai.ChatCompletion.create( model="gpt-3.5-turbo", messages=[{"role": "user", "content": "Hello"}] )这就像寄快递只写“收件人:张三”,却不填地址、电话、物品类型。API 请求的每个字段,都是与模型达成的隐式契约。
3.1messages数组:角色、内容、顺序的三重语义约束
messages不是消息列表,而是对话状态机的快照。OpenAI 模型严格按数组顺序理解上下文,且对role值有硬性要求:
| role | 含义 | 必须性 | 典型场景 |
|---|---|---|---|
system | 设定模型行为准则(如“你是一个严谨的数学老师”) | 可选,但强烈建议 | 控制语气、知识边界、输出格式 |
user | 用户输入内容 | 必须至少一个 | 所有提问、指令、数据输入 |
assistant | 模型历史回复(用于多轮对话续写) | 仅当需要上下文记忆时必填 | 聊天机器人、客服对话流 |
tool | 工具调用返回结果(v1.0+ 新增) | 仅当使用 function calling 时 | 外部 API 结果注入 |
关键陷阱:
system消息位置:必须放在messages数组首位。若插在中间(如[user, system, user]),模型会将其视为普通用户消息,失去系统指令效力。assistant消息内容:不能包含role: "assistant"的原始回复文本(如"The answer is 42"),而应是模型实际返回的message.content字段值。若手动拼接,易引入格式错乱。- 中文 content 的编码风险:
content中含 emoji 或生僻汉字时,需确保 Python 字符串为 UTF-8 编码,且 HTTP 请求头Content-Type: application/json; charset=utf-8被正确设置(openai库自动处理,但自定义请求需注意)。
实操示例:构建一个“代码审查助手”的messages:
messages = [ { "role": "system", "content": "你是一名资深 Python 工程师,专注于 PEP 8 规范、性能优化和安全漏洞识别。请用中文回复,分三部分:1) 问题描述;2) 修复建议;3) 修改后代码(仅输出代码块,不加解释)。" }, { "role": "user", "content": "请审查以下代码:\n```python\ndef calc(a, b):\n return a + b\n```" } ]这里system指令明确界定了角色、语言、输出结构,大幅降低模型自由发挥导致的格式混乱。
3.2model参数:选择不是型号,而是选择计算资源与能力边界的组合
当前主流模型能力矩阵(截至 2024 年 7 月):
| Model | 上下文窗口 | 输入/输出计费粒度 | 强项 | 弱项 | 推荐场景 |
|---|---|---|---|---|---|
gpt-3.5-turbo-0125 | 16K tokens | 按 token 计费 | 通用对话、简单代码生成 | 复杂逻辑推理、长文档摘要 | 个人项目、低频客服 |
gpt-4o-2024-05-13 | 128K tokens | 按 token 计费 | 多模态理解、实时语音、超长上下文 | 成本高、响应延迟略高 | 专业分析、文档处理、音视频理解 |
gpt-4-turbo-preview | 128K tokens | 按 token 计费 | 代码能力、数学推理 | 需显式指定response_format={"type": "json_object"}才稳定输出 JSON | 结构化数据生成、API 响应构造 |
gpt-4o-mini | 128K tokens | 成本最低 | 快速响应、轻量任务 | 事实准确性略低于 gpt-4o | 移动端集成、高频轻量查询 |
关键决策点:
- 不要盲目追新:
gpt-4o并非在所有场景都优于gpt-3.5-turbo。实测显示,在纯文本摘要任务中,gpt-3.5-turbo-0125的单位 token 准确率更高,因其训练数据更聚焦于文本压缩。 - 警惕“context window”陷阱:
128K是理论最大值,实际可用值受max_tokens限制。若请求中messages总长度已达120K,再设max_tokens=8192,必然触发context window limit错误。正确做法是动态计算剩余空间:
def calculate_max_tokens(messages: list, model: str = "gpt-4o") -> int: # 粗略估算:每字符约 1.3 tokens(英文),中文约 2.0 tokens total_chars = sum(len(m["content"]) for m in messages) estimated_tokens = int(total_chars * (2.0 if any('\u4e00' <= c <= '\u9fff' for m in messages for c in m["content"]) else 1.3)) # 模型最大上下文 - 已用 tokens - 保留 200 tokens 给响应 context_limits = {"gpt-3.5-turbo-0125": 16384, "gpt-4o": 131072} max_context = context_limits.get(model, 16384) return max(100, min(4096, max_context - estimated_tokens - 200))model字符串必须精确匹配:gpt-4o与gpt-4o-2024-05-13是不同模型,后者是前者的一个具体快照版本。若账号未开通新版,调用gpt-4o可能返回404。
3.3 温度(temperature)与 Top-p(top_p):控制随机性的物理旋钮
这两个参数常被误解为“让回答更有趣”或“更准确”,实则是调节模型采样分布的物理参数:
temperature:控制 logits 分布的“尖锐度”。值越低(如0.0),模型越倾向于选择概率最高的 token,输出确定性强但可能僵化;值越高(如1.0),分布越平滑,输出多样性高但可能离题。工程实践中,0.3~0.7是平衡点。例如:- 生成 SQL 查询:
temperature=0.0,确保语法绝对正确; - 创意文案生成:
temperature=0.8,激发多样性; - 代码补全:
temperature=0.2,兼顾准确与自然。
- 生成 SQL 查询:
top_p(核采样):设定累积概率阈值。top_p=0.9表示只从概率总和占前 90% 的 tokens 中采样,自动过滤低概率噪声。它与temperature协同工作:temperature调整分布形状,top_p截断分布尾部。
实测对比(同一 prompt 下gpt-3.5-turbo输出):
| temperature | top_p | 输出特征 |
|---|---|---|
| 0.0 | 1.0 | 重复率高,如连续三次输出“好的,我明白了” |
| 0.7 | 0.9 | 语句流畅,逻辑连贯,偶有小创意 |
| 1.0 | 0.5 | 用词生僻,出现虚构术语(如“量子递归算法”) |
经验:在生产环境,永远显式设置
temperature和top_p。不设时默认temperature=1.0, top_p=1.0,相当于开启“完全随机模式”,导致相同输入产生不可复现输出,给日志追踪和问题复现带来灾难。
4. 错误处理不是 try-except,而是构建一套可诊断的故障响应体系
OpenAI API 的错误不是简单的网络异常,而是分层的业务语义错误。直接except Exception as e:捕获,等于蒙眼开车。
4.1 OpenAI 错误码的四级分类与根因映射
| HTTP 状态码 | 错误类型 | 典型 message | 根因分析 | 应对策略 |
|---|---|---|---|---|
400 | 请求参数错误 | "this model's maximum context length is 1048565 tokens" | messages+max_tokens超出模型上下文窗口 | 动态计算max_tokens,截断长消息 |
401 | 认证失败 | "Incorrect API key provided" | 密钥无效、过期、格式错误 | 检查密钥格式、轮换状态、环境变量加载路径 |
402 | 支付失败 | "Insufficient balance" | 账户余额不足、订阅计划到期、信用卡扣款失败 | 登录 dashboard 检查账单、升级计划、更新支付方式 |
403 | 权限拒绝 | "You don't have access to this model" | 账户未开通该模型权限(如 gpt-4 需申请) | 提交模型访问申请、检查组织成员权限 |
404 | 资源不存在 | "The model does not exist" | model字符串拼写错误、模型已下线 | 核对 OpenAI Models 文档,使用最新名称 |
429 | 速率限制 | "Too many requests" | 超出每分钟请求数(RPM)或每分钟 token 数(TPM)配额 | 实施指数退避重试、拆分批量请求、升级配额 |
500 | 服务端错误 | "Internal server error" | OpenAI 后端临时故障 | 立即重试(最多 3 次),记录错误时间戳供后续反馈 |
关键洞察:400和429是最高频的两类错误,合计占比超 75%。其中400错误中,context window limit又占400类的 60% 以上——根源几乎全是messages长度过大或max_tokens设置不合理。
4.2 构建结构化错误捕获与日志体系
标准try-except无法区分语义。正确做法是捕获openai.APIError的子类,并提取结构化信息:
from openai import APIError, RateLimitError, AuthenticationError, BadRequestError def safe_chat_completion(**kwargs): try: response = client.chat.completions.create(**kwargs) return response except BadRequestError as e: # 解析 OpenAI 返回的 error 字段 error_detail = e.body.get("error", {}) error_type = error_detail.get("type", "unknown") message = error_detail.get("message", str(e)) # 按 type 分类处理 if "context_length" in message.lower(): # 触发上下文截断逻辑 truncated_messages = truncate_messages(kwargs["messages"], model=kwargs.get("model", "gpt-3.5-turbo")) kwargs["messages"] = truncated_messages return safe_chat_completion(**kwargs) # 递归重试 elif "invalid_request_error" in error_type: logger.error(f"Bad Request: {message} | Params: {kwargs}") raise except RateLimitError as e: # 指数退避重试 retry_after = int(e.response.headers.get("retry-after", "1")) time.sleep(min(retry_after * (2 ** attempt), 60)) # 最大等待 60 秒 return safe_chat_completion(**kwargs) except AuthenticationError as e: logger.critical(f"Auth Failed: {e} | Check API key and env loading") raise except Exception as e: logger.exception("Unexpected error in chat completion") raise日志必须包含:
- 请求指纹:
model+len(messages)+sum(len(m['content']) for m in messages); - 错误元数据:HTTP 状态码、
error.type、error.code(如有)、retry-after头; - 上下文快照:截取
messages[0]['content'][:100]和messages[-1]['content'][:100],避免日志过大。
提示:我在一个金融问答项目中,曾发现
429错误集中出现在每日 9:30-10:00(A股开盘时段)。通过日志分析,确认是前端未做请求节流,用户点击“分析报告”按钮后,同时触发 5 个并行请求。解决方案不是加重试,而是前端增加Promise.allSettled+ 限流器,将并发数压到 2。
4.3 重试策略:不是“多试几次”,而是基于错误类型的智能退避
通用重试(如tenacity库)对401、402无效——重试一万次,密钥错误还是错误。真正的重试只针对瞬时可恢复错误:429、500、502、503、504。
推荐策略(基于tenacity):
from tenacity import retry, stop_after_attempt, wait_exponential, retry_if_exception_type @retry( stop=stop_after_attempt(3), wait=wait_exponential(multiplier=1, min=1, max=10), # 1s, 2s, 4s retry=retry_if_exception_type((RateLimitError, APIConnectionError, APITimeoutError)), reraise=True ) def robust_chat_completion(**kwargs): return client.chat.completions.create(**kwargs)stop_after_attempt(3):超过 3 次仍失败,放弃,避免雪崩;wait_exponential:首次等待 1 秒,第二次 2 秒,第三次 4 秒,防止重试风暴;retry_if_exception_type:精准限定重试范围,AuthenticationError等绝不重试。
5. 响应解析不是取response.choices[0].message.content,而是构建抗扰动的数据管道
拿到response对象后,90% 的代码直接取content。但生产环境中,content可能为空、可能含非法字符、可能被截断、可能因流式响应未收全——这导致下游 JSON 解析崩溃、前端渲染空白、数据库写入失败。
5.1response对象的完整结构与关键字段防御性读取
一个典型ChatCompletion响应结构(精简):
{ "id": "chatcmpl-xxx", "object": "chat.completion", "created": 1719823456, "model": "gpt-3.5-turbo-0125", "choices": [ { "index": 0, "message": { "role": "assistant", "content": "Hello! How can I help you today?" }, "finish_reason": "stop", "logprobs": null } ], "usage": { "prompt_tokens": 15, "completion_tokens": 12, "total_tokens": 27 } }必须防御性读取的字段:
response.choices:必须检查长度。len(response.choices) == 0表示无有效回复(罕见,但可能因n > 1且部分失败);response.choices[0].message.content:必须检查是否为 None 或空字符串。模型可能因finish_reason="length"(达到max_tokens)而提前终止,content为空;response.choices[0].finish_reason:指示终止原因,关键值:"stop":正常结束;"length":达到max_tokens限制,内容被截断;"content_filter":触发安全过滤器,内容被屏蔽(此时content可能为None或空);"tool_calls":函数调用模式下,表示已生成工具调用指令。
防御性解析函数:
def parse_response(response) -> dict: if not response.choices: raise ValueError("No choices in response") choice = response.choices[0] content = choice.message.content # 处理 content_filter 场景 if choice.finish_reason == "content_filter": return { "text": "", "is_filtered": True, "reason": "content_filter", "usage": getattr(response, "usage", {}) } # 处理 length 截断 if choice.finish_reason == "length": # 记录警告,但不抛异常,业务层可决定是否重试 logger.warning(f"Response truncated by max_tokens. Usage: {response.usage}") # 清理 content:移除首尾空白,替换不可见控制字符 if isinstance(content, str): content = re.sub(r"[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]", "", content.strip()) return { "text": content or "", "is_filtered": False, "finish_reason": choice.finish_reason, "usage": { "prompt_tokens": response.usage.prompt_tokens, "completion_tokens": response.usage.completion_tokens, "total_tokens": response.usage.total_tokens } } # 使用 result = parse_response(response) if result["is_filtered"]: print("内容被安全策略过滤") else: print("回复内容:", result["text"])5.2 流式响应(stream=True)的完整消费模式
流式响应不是“更快”,而是更低延迟、更可控的内存占用。但它的消费逻辑极易出错。
错误写法(常见于教程):
# ❌ 错误:假设 stream 总是返回完整 content for chunk in response: print(chunk.choices[0].delta.content) # delta.content 可能为 None!正确写法(必须累积):
def consume_stream(response): full_content = "" for chunk in response: # 检查 choices 是否存在且非空 if not chunk.choices: continue delta = chunk.choices[0].delta # delta.content 可能为 None(如首块只含 role) if delta.content is not None: full_content += delta.content # 实时输出(如 CLI 进度条) print(delta.content, end="", flush=True) return full_content # 使用 stream_response = client.chat.completions.create( model="gpt-3.5-turbo", messages=[{"role": "user", "content": "讲个笑话"}], stream=True ) final_text = consume_stream(stream_response)关键点:
chunk.choices[0].delta是一个ChatCompletionChunk.Choice.Delta对象,其content字段在流式传输中分片发送,首块可能只有role="assistant",后续块才带content;- 必须用
+=累积,不能只取最后一块; flush=True确保print实时输出,避免缓冲区阻塞。
5.3 本地响应缓存:减少重复请求,提升用户体验
对固定 prompt(如系统指令、FAQ 回答),可构建 LRU 缓存。但需注意:
- 缓存键必须包含所有影响输出的参数:
model+temperature+top_p+messages的哈希值(而非字符串,避免长消息哈希慢); - 缓存值必须包含完整
response对象或结构化字典,而非仅content,以便复用usage等元数据; - 设置合理 TTL:
gpt-3.5-turbo模型更新频繁,缓存不宜超过 24 小时。
简易缓存实现(使用functools.lru_cache):
from functools import lru_cache import hashlib @lru_cache(maxsize=128) def cached_chat_completion_hashed( model: str, temperature: float, top_p: float, messages_hash: str # 预先计算的 messages 字符串 hash ): # 实际调用 API response = client.chat.completions.create( model=model, temperature=temperature, top_p=top_p, messages=deserialize_messages(messages_hash) # 反序列化 ) return { "content": response.choices[0].message.content, "usage": response.usage.dict() } # 使用前计算 hash def hash_messages(messages: list) -> str: # 将 messages 转为规范 JSON 字符串(排序 key,无空格) json_str = json.dumps(messages, sort_keys=True, separators=(',', ':')) return hashlib.md5(json_str.encode()).hexdigest() # 调用 msg_hash = hash_messages(messages) result = cached_chat_completion_hashed( model="gpt-3.5-turbo", temperature=0.3, top_p=0.9, messages_hash=msg_hash )6. 完整可运行代码:不是 Demo,而是生产就绪的最小可行模块
以下是经过上述所有原则验证的、可直接用于生产环境的完整模块。它不是一个“hello world”,而是一个具备密钥安全、错误分类、重试、缓存、日志的最小可行单元。
# chatgpt_client.py """ OpenAI ChatGPT API 生产就绪客户端 - 密钥安全加载与校验 - 结构化错误处理与日志 - 智能重试策略 - 响应解析与流式消费 - 本地 LRU 缓存(可选) """ import os import time import json import hashlib import logging from typing import List, Dict, Optional, Any, Generator from functools import lru_cache # 配置日志 logging.basicConfig( level=logging.INFO, format="%(asctime)s - %(name)s - %(levelname)s - %(message)s", handlers=[logging.StreamHandler()] ) logger = logging.getLogger("chatgpt_client") # --- 1. 密钥与客户端初始化 --- try: from dotenv import load_dotenv load_dotenv(dotenv_path=".env") except ImportError: pass import openai from openai import OpenAI, APIError, RateLimitError, AuthenticationError, BadRequestError, APIConnectionError, APITimeoutError # 初始化客户端(支持 base_url 用于中转代理) client = OpenAI( api_key=os.getenv("OPENAI_API_KEY"), base_url=os.getenv("OPENAI_BASE_URL", "https://api.openai.com/v1"), timeout=30.0, max_retries=0, # 由自定义重试逻辑接管 ) # 密钥校验 api_key = os.getenv("OPENAI_API_KEY") if not api_key or not isinstance(api_key, str) or not api_key.startswith("sk-"): raise ValueError("OPENAI_API_KEY is invalid or not set. Please check your .env file.") # --- 2. 工具函数 --- def hash_messages(messages: List[Dict[str, str]]) -> str: """计算 messages 的确定性 hash,用于缓存键""" json_str = json.dumps(messages, sort_keys=True, separators=(',', ':')) return hashlib.md5(json_str.encode()).hexdigest() def truncate_messages( messages: List[Dict[str, str]], model: str = "gpt-3.5-turbo", max_context: int = 16384 ) -> List[Dict[str, str]]: """按模型上下文窗口截断 messages,保留 system + 最近 user/assistant 对""" # 简化估算:每字符 ~2 tokens(中文为主) total_chars = sum(len(m.get("content", "")) for m in messages) estimated_tokens = int(total_chars * 2.0) if estimated_tokens <= max_context * 0.8: # 保留 20% 余量 return messages # 保留 system 消息(如果存在) system_msg = None non_system_msgs = [] for m in messages: if m.get("role") == "system": system_msg = m else: non_system_msgs.append(m) # 保留最近的 3 轮对话(6 条消息) kept_msgs = non_system_msgs[-6:] if len(non_system_msgs) > 6 else non_system_msgs if system_msg: kept_msgs = [system_msg] + kept_msgs logger.warning(f"Truncated messages from {len(messages)} to {len(kept_msgs)} due to context limit.") return kept_msgs # --- 3. 核心 API 调用函数 --- @lru_cache(maxsize=128) def _cached_completion( model: str, temperature: float, top_p: float, messages_hash: str, max_tokens: Optional[int] = None ) -> Dict[str, Any]: """缓存层:仅缓存确定性参数组合""" # 此处不实际调用 API,仅为演示缓存结构 # 实际项目中,此处应调用 _robust_completion 并