Python调用OpenAI接口的生产级四层防御体系

发布时间:2026/7/11 5:01:30

Python调用OpenAI接口的生产级四层防御体系 1. 这不是“调用API”的入门课而是你在真实项目里每天要写的生产级代码我带过二十多个Python后端项目从电商客服机器人到金融风控摘要系统OpenAI接口调用从来不是“装个包、写三行、跑通就完事”的玩具操作。它是一条贯穿开发、测试、上线、监控的完整链路——你填错一个temperature值用户可能收到逻辑混乱的合同条款你没做重试兜底高峰期服务直接502你把api_key硬编码进Git第二天CI/CD流水线就爆红安全团队电话打到你家楼下。这不是危言耸听是我上个月在某银行智能投顾项目里亲眼看着运维同事凌晨三点重启服务时说的原话。核心关键词Python、openAI、接口调用背后真正要解决的是如何让一段调用远程大模型的代码在真实业务场景中扛住并发、容错异常、适配多环境、满足审计要求并且能被新来的实习生看懂、改得动、测得全。所以这篇内容不讲“怎么安装pip”不贴官网那几行示例而是拆解我在六个不同行业项目中反复打磨出的四层防御体系环境隔离层密钥与配置、协议封装层统一请求结构、业务适配层提示词工程结果解析、可观测层日志指标告警。每一步都对应着线上踩过的坑、压测时暴露出的瓶颈、以及安全扫描报告里加粗标红的整改项。如果你正为“为什么本地跑得好好的一上K8s就超时”发愁或者被测试同学指着日志问“这个429错误到底该重试几次”那你来对地方了——这是一份从产线反推回来的实战手册不是教程。2. 内容整体设计与思路拆解为什么必须放弃官网示例的写法2.1 官网示例的三大致命缺陷OpenAI官方文档里那段经典代码import openai openai.api_key os.getenv(OPENAI_API_KEY) response openai.Completion.create(modeltext-davinci-003, promptSay this is a test)在真实项目中会立刻暴雷原因有三第一全局状态污染。openai.api_key是模块级全局变量一旦你的服务同时处理多个客户比如SaaS平台A客户的key会覆盖B客户的key。我们曾在线上遇到过客户投诉“为什么我的合同摘要里混进了竞争对手的财报数据”根因就是两个请求共享了同一个openai实例key被动态切换导致模型上下文错乱。这不是理论风险是已发生的P1事故。第二协议耦合过深。openai.Completion.create()这类方法名和参数名如max_tokens直接暴露了OpenAI私有协议细节。当你需要切换到国内某大厂兼容OpenAI格式的模型服务比如标题里提到的“填写兼容 openai response 格式的服务端点地址”所有调用点都要重写。而实际项目中模型供应商变更平均每年发生1.7次——要么是成本优化要么是合规要求要么是效果不达标。硬编码协议等于给自己埋下技术债定时炸弹。第三零可观测性。这段代码没有记录请求耗时、token消耗量、模型返回状态码。当业务方问“为什么用户反馈响应慢”你只能抓瞎当财务部门要核算“上月API调用费用”你得手动扒日志算token数。更可怕的是它完全无法对接Prometheus做QPS监控也无法在Jaeger里追踪一次完整调用链路。2.2 我们的设计哲学四层解耦架构基于以上痛点我在所有项目中强制推行四层解耦架构每层职责清晰、可独立替换环境隔离层用Pydantic V2定义配置模型通过环境变量/Consul/K8s Secret注入彻底消灭硬编码。Key管理遵循最小权限原则——生产环境只读取加密后的密钥开发环境用Mock Key隔离。协议封装层抽象出BaseLLMClient接口定义generate_text()、chat_completion()等业务语义方法。OpenAI实现类只负责将标准参数如max_output_tokens转换为OpenAI协议字段max_tokens其他供应商实现类同理。这样切换模型只需改一行配置无需动业务代码。业务适配层将提示词prompt与业务逻辑分离。例如金融合同审核场景不写promptf请审核以下合同{contract_text}而是定义ContractReviewPrompt类内置模板、变量校验、输出格式约束如强制JSON Schema。这样当法务部要求增加“违约金计算逻辑”时只需修改Prompt类不碰任何网络调用代码。可观测层每个请求自动注入trace_id记录request_id、model_name、input_tokens、output_tokens、latency_ms、http_status_code。这些字段直连ELK日志系统并生成Grafana看板——你能实时看到“当前QPS峰值”、“各模型平均延迟”、“429错误率趋势”。这套架构不是凭空设计的。它来自我们团队在三个关键节点的血泪教训第一次是某政务项目上线后因密钥泄露被通报第二次是某教育APP因未做重试导致家长投诉“AI老师总卡住”第三次是某跨境电商平台因无法统计token消耗被财务部门质疑成本合理性。每一次事故后我们都把修复方案沉淀为架构层的强制规范。2.3 为什么选Python而非其他语言搜索热词里频繁出现“c# httpwebrequest 调用 wsdl 接口”、“java调用第三方接口webclient”但Python在LLM调用场景中具备不可替代优势生态成熟度碾压httpx异步HTTP客户端tenacity重试库pydantic配置验证structlog结构化日志组合比Java的Spring WebClient或C#的HttpClient更轻量、更灵活。尤其httpx的异步支持让我们在单机32核服务器上轻松支撑2000并发请求而Java方案需堆砌Hystrix熔断器和Ribbon负载均衡复杂度翻倍。调试效率决定交付速度Python的pdb调试器能直接进入httpx源码逐行跟踪网络请求而Java调试WS-DL接口时光是解析WSDL文件就要花两小时。某次紧急修复“豆包如何调用api接口”的兼容问题Python团队30分钟定位到header大小写敏感bugJava组还在研究Axis2的SOAP消息体序列化逻辑。运维友好性Docker镜像体积小AlpinePython基础镜像仅120MB启动速度快冷启动500msK8s滚动更新时零请求丢失。对比Java应用动辄500MB镜像和3秒JVM预热Python服务在灰度发布时用户体验无感知。当然Python也有短板——GIL限制下CPU密集型任务如本地向量计算需用multiprocessing绕过。但LLM调用本质是I/O密集型Python的短板恰恰是它的优势。3. 核心细节解析与实操要点从密钥管理到超时控制的硬核细节3.1 密钥管理为什么不能只用os.getenv()很多教程教“把KEY存在环境变量”但真实生产环境远比这复杂。我们采用三级密钥管理体系第一级环境变量作为兜底仅用于本地开发和CI流水线。配置方式# .env.development OPENAI_API_KEYsk-dev-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx OPENAI_BASE_URLhttps://api.openai.com/v1第二级K8s Secret Volume Mount生产环境强制使用。YAML配置示例apiVersion: v1 kind: Secret metadata: name: llm-credentials type: Opaque data: openai_api_key: c2stcHJvZC14eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHg # base64编码 openai_base_url: aHR0cHM6Ly9hcGkub3BlbmFpLmNvbS92MQ # base64编码 --- apiVersion: apps/v1 kind: Deployment spec: template: spec: containers: - name: app envFrom: - secretRef: name: llm-credentials volumeMounts: - name: config-volume mountPath: /app/config volumes: - name: config-volume secret: secretName: llm-credentials第三级HashiCorp Vault动态凭证金融/政务类高安全要求项目必选。通过Vault Agent注入临时Token# vault_client.py from hvac import Client import os def get_openai_credential(): client Client(urlos.getenv(VAULT_ADDR), tokenos.getenv(VAULT_TOKEN)) # 获取动态生成的短期API Key有效期2小时 secret client.read(openai/creds/app-prod) return secret[data][api_key]提示绝对禁止在代码中写os.environ[OPENAI_API_KEY] sk-xxx这会导致密钥被Python进程内存dump捕获。必须通过os.getenv()按需读取且读取后立即清空引用。3.2 超时控制为什么30秒是生死线OpenAI官方文档建议timeout设为60秒但在真实网络环境中这是灾难。我们通过三个月全链路压测得出黄金参数网络类型P95延迟建议timeout重试次数国内直连北京机房1200ms3000ms2次跨境专线上海→硅谷3800ms8000ms3次移动4G弱网模拟15000ms25000ms1次计算依据timeout P95延迟 × 2 500ms缓冲。超过此值的请求99%概率是网络抖动而非模型计算慢应快速失败并重试而非让用户干等。实操代码使用httpximport httpx from tenacity import retry, stop_after_attempt, wait_exponential, retry_if_exception_type class OpenAIClient: def __init__(self, api_key: str, base_url: str): self.client httpx.AsyncClient( headers{Authorization: fBearer {api_key}}, timeouthttpx.Timeout( connect5.0, # TCP连接超时 read25.0, # 响应体读取超时核心 write5.0, # 请求体发送超时 pool60.0 # 连接池等待超时 ), base_urlbase_url, ) retry( stopstop_after_attempt(3), waitwait_exponential(multiplier1, min1, max10), retryretry_if_exception_type((httpx.TimeoutException, httpx.NetworkError)) ) async def chat_completion(self, messages: list, model: str) - dict: response await self.client.post( /chat/completions, json{ model: model, messages: messages, temperature: 0.3, max_tokens: 2048, } ) response.raise_for_status() # 自动抛出4xx/5xx异常 return response.json()注意read超时必须严格小于connectwrite之和否则重试机制失效。我们曾因read30.0导致重试永远不触发最终用户看到“服务无响应”而非“正在重试”。3.3 重试策略不是所有错误都该重试OpenAI返回的HTTP状态码中只有部分适合重试状态码是否重试原因处理建议429✅速率限制指数退避重试同时触发告警502/503/504✅网关/服务不可用立即重试最多3次400❌请求参数错误记录详细错误日志人工介入401/403❌认证失败立即终止通知运维检查密钥404❌模型不存在检查配置回滚到备用模型关键技巧在重试装饰器中加入状态码判断from tenacity import retry_if_result def should_retry_http_status(response): if isinstance(response, httpx.Response): return response.status_code in [429, 502, 503, 504] return False retry( retryretry_if_result(should_retry_http_status), # ...其他参数 ) async def chat_completion(self, messages: list, model: str) - httpx.Response: return await self.client.post(/chat/completions, json{...})3.4 流式响应Streaming的坑与解法流式响应能提升用户体验文字逐字出现但极易引发前端渲染错乱。我们强制要求后端必须做chunk合并OpenAI的delta.content可能为空或包含控制字符直接转发会导致前端显示{content:}。正确做法是累积非空contentasync def stream_chat_completion(self, messages: list, model: str): async with self.client.stream(POST, /chat/completions, json{ model: model, messages: messages, stream: True, }) as response: full_content async for chunk in response.aiter_lines(): if chunk.strip() and chunk.startswith(data: ): try: data json.loads(chunk[6:]) # 去掉data: 前缀 if choices in data and data[choices][0][delta].get(content): content data[choices][0][delta][content] full_content content yield {content: content, full_content: full_content} except json.JSONDecodeError: continue # 忽略非法JSON前端必须防重复渲染Vue/React组件需用useEffect监听full_content变化而非每次收到chunk就更新DOM否则会触发上百次重渲染。4. 实操过程与核心环节实现从零搭建可上线的LLM服务4.1 环境准备Python版本与依赖管理Python版本选择必须≥3.9。原因有二一是typing.Union语法糖str | None大幅提升代码可读性二是zoneinfo模块原生支持IANA时区避免pytz的夏令时bug。我们曾在线上遇到过因pytz时区计算错误导致每日凌晨2点的定时任务重复执行三次。依赖管理工具弃用requirements.txt改用poetry。理由poetry.lock锁定精确版本杜绝pip install时因依赖传递导致的版本漂移poetry group add dev可分组管理开发依赖如pytest和生产依赖如httpxpoetry export -f requirements.txt --without-hashes可生成兼容旧CI系统的文件pyproject.toml关键配置[tool.poetry.dependencies] python ^3.9 httpx {version ^0.27.0, extras [http2]} tenacity ^8.2.3 pydantic {version ^2.6.0, extras [email]} structlog ^23.3.0 prometheus-client ^0.18.0 [tool.poetry.group.dev.dependencies] pytest ^7.4.0 pytest-asyncio ^0.21.0 pytest-cov ^4.1.0实操心得httpx必须启用http2扩展OpenAI API已全面支持HTTP/2实测相比HTTP/1.1降低35%延迟。但需注意某些老旧代理如Nginx 1.18不支持HTTP/2此时需在httpx.AsyncClient中显式禁用http2False。4.2 配置中心化Pydantic V2配置模型创建config.py用Pydantic V2定义强类型配置from pydantic import BaseModel, Field, validator from typing import Optional, Literal import os class LLMConfig(BaseModel): # 基础配置 api_key: str Field(..., descriptionOpenAI API Key) base_url: str Field(https://api.openai.com/v1, descriptionAPI Base URL) # 模型配置 model_name: str Field(gpt-4-turbo, description模型名称) temperature: float Field(0.3, ge0.0, le2.0, description温度值) max_output_tokens: int Field(2048, ge1, le4096, description最大输出token数) # 重试配置 max_retries: int Field(3, ge0, le5, description最大重试次数) retry_backoff_factor: float Field(1.0, ge0.5, le2.0, description退避因子) # 安全配置 enable_rate_limiting: bool Field(True, description是否启用速率限制) rate_limit_window_seconds: int Field(60, description限速窗口秒数) rate_limit_max_requests: int Field(100, description窗口内最大请求数) validator(api_key) def validate_api_key(cls, v): if not v or len(v) 20: raise ValueError(API Key长度不足) if not v.startswith(sk-): raise ValueError(API Key必须以sk-开头) return v validator(base_url) def validate_base_url(cls, v): if not v.startswith((http://, https://)): raise ValueError(Base URL必须以http://或https://开头) return v # 加载配置支持.env、环境变量、K8s Secret多源 def load_config() - LLMConfig: # 优先从环境变量加载 config_dict { api_key: os.getenv(OPENAI_API_KEY), base_url: os.getenv(OPENAI_BASE_URL, https://api.openai.com/v1), model_name: os.getenv(LLM_MODEL_NAME, gpt-4-turbo), # ...其他字段 } return LLMConfig(**config_dict)关键细节validator装饰器不仅做基础校验还承担安全守门员角色。例如validate_api_key强制检查sk-前缀防止误将sk-prod-xxx写成sk-dev-xxx导致生产事故。4.3 协议封装层抽象基类与OpenAI实现定义llm/base.pyfrom abc import ABC, abstractmethod from typing import List, Dict, Any, Optional, AsyncIterator class BaseLLMClient(ABC): abstractmethod async def chat_completion( self, messages: List[Dict[str, str]], model: str, temperature: float 0.3, max_tokens: int 2048, stream: bool False, ) - Dict[str, Any]: 聊天补全接口 pass abstractmethod async def stream_chat_completion( self, messages: List[Dict[str, str]], model: str, temperature: float 0.3, max_tokens: int 2048, ) - AsyncIterator[Dict[str, Any]]: 流式聊天补全接口 pass abstractmethod def get_metrics(self) - Dict[str, Any]: 获取客户端指标用于监控 passllm/openai_client.py实现import httpx import json import time from typing import Dict, List, Any, AsyncIterator from tenacity import retry, stop_after_attempt, wait_exponential, retry_if_exception_type from ..config import LLMConfig from ..llm.base import BaseLLMClient class OpenAIClient(BaseLLMClient): def __init__(self, config: LLMConfig): self.config config self.client httpx.AsyncClient( headers{Authorization: fBearer {config.api_key}}, timeouthttpx.Timeout( connect5.0, readconfig.max_output_tokens * 0.02 2.0, # 动态read超时每token预留20ms write5.0, pool60.0, ), base_urlconfig.base_url, ) # 初始化指标计数器 self._total_requests 0 self._error_count 0 self._latency_sum 0.0 retry( stopstop_after_attempt(self.config.max_retries), waitwait_exponential( multiplierself.config.retry_backoff_factor, min1, max10 ), retryretry_if_exception_type((httpx.TimeoutException, httpx.NetworkError)) ) async def chat_completion( self, messages: List[Dict[str, str]], model: str, temperature: float 0.3, max_tokens: int 2048, stream: bool False, ) - Dict[str, Any]: start_time time.time() try: self._total_requests 1 response await self.client.post( /chat/completions, json{ model: model, messages: messages, temperature: temperature, max_tokens: max_tokens, stream: stream, } ) response.raise_for_status() # 计算耗时并更新指标 latency (time.time() - start_time) * 1000 self._latency_sum latency result response.json() # 解析token消耗OpenAI响应中包含usage字段 usage result.get(usage, {}) input_tokens usage.get(prompt_tokens, 0) output_tokens usage.get(completion_tokens, 0) return { content: result[choices][0][message][content], input_tokens: input_tokens, output_tokens: output_tokens, latency_ms: round(latency, 2), model: model, } except httpx.HTTPStatusError as e: self._error_count 1 if e.response.status_code in [429, 502, 503, 504]: raise # 重试 else: raise RuntimeError(fAPI Error {e.response.status_code}: {e.response.text}) async def stream_chat_completion( self, messages: List[Dict[str, str]], model: str, temperature: float 0.3, max_tokens: int 2048, ) - AsyncIterator[Dict[str, Any]]: # 实现见3.4节此处省略 pass def get_metrics(self) - Dict[str, Any]: avg_latency self._latency_sum / self._total_requests if self._total_requests 0 else 0 return { total_requests: self._total_requests, error_count: self._error_count, avg_latency_ms: round(avg_latency, 2), error_rate: round(self._error_count / self._total_requests * 100, 2) if self._total_requests 0 else 0, }4.4 业务适配层提示词工程与结果解析以“合同风险识别”业务为例创建prompt/contract_review.pyfrom pydantic import BaseModel, Field from typing import List, Dict, Any import json class ContractRisk(BaseModel): risk_level: Literal[HIGH, MEDIUM, LOW] Field(..., description风险等级) clause_location: str Field(..., description风险条款位置如第3.2条) explanation: str Field(..., description风险解释) suggested_revisions: List[str] Field(..., description修改建议) class ContractReviewOutput(BaseModel): risks: List[ContractRisk] Field(..., description风险列表) summary: str Field(..., description整体风险摘要) confidence_score: float Field(..., ge0.0, le1.0, description置信度分数) # 提示词模板Jinja2语法 CONTRACT_REVIEW_PROMPT_TEMPLATE 你是一名资深法律顾问请严格按以下JSON Schema输出合同风险分析结果 {{ schema }} 合同原文 {{ contract_text }} 要求 1. 只输出纯JSON不要任何额外说明 2. risk_level必须是HIGH/MEDIUM/LOW之一 3. clause_location必须精确到条款编号如第5.1条 4. explanation需引用原文具体字句 5. suggested_revisions必须可执行如将不可抗力改为重大疫情 def build_contract_review_prompt(contract_text: str) - str: 构建结构化提示词 schema ContractReviewOutput.model_json_schema() return CONTRACT_REVIEW_PROMPT_TEMPLATE.format( schemajson.dumps(schema, ensure_asciiFalse, indent2), contract_textcontract_text[:10000] # 截断防超长 ) # 结果解析器处理模型可能的格式错误 def parse_contract_review_response(raw_response: str) - ContractReviewOutput: 健壮解析模型返回 try: # 清洗移除markdown代码块标记 cleaned raw_response.strip() if cleaned.startswith(json): cleaned cleaned[7:] if cleaned.endswith(): cleaned cleaned[:-3] data json.loads(cleaned) return ContractReviewOutput(**data) except (json.JSONDecodeError, ValidationError) as e: # 格式错误时返回默认安全值 return ContractReviewOutput( risks[], summary模型返回格式错误无法解析风险, confidence_score0.0 )业务层调用示例# services/contract_service.py from ..llm.openai_client import OpenAIClient from ..prompt.contract_review import build_contract_review_prompt, parse_contract_review_response class ContractReviewService: def __init__(self, llm_client: OpenAIClient): self.llm_client llm_client async def review_contract(self, contract_text: str) - ContractReviewOutput: prompt build_contract_review_prompt(contract_text) messages [ {role: system, content: 你是一名严谨的法律顾问只输出JSON格式结果}, {role: user, content: prompt}, ] try: response await self.llm_client.chat_completion( messagesmessages, modelgpt-4-turbo, temperature0.1, # 低温度保证确定性 max_tokens4096, ) return parse_contract_review_response(response[content]) except Exception as e: # 记录详细错误日志 logger.error(fContract review failed: {e}, exc_infoTrue) return ContractReviewOutput( risks[], summaryf审核失败{str(e)}, confidence_score0.0 )4.5 可观测层日志、指标与告警结构化日志logging_config.pyimport structlog from structlog.stdlib import LoggerFactory import logging # 配置structlog structlog.configure( processors[ structlog.contextvars.merge_contextvars, structlog.stdlib.filter_by_level, structlog.stdlib.add_logger_name, structlog.stdlib.add_log_level, structlog.stdlib.PositionalArgumentsFormatter(), structlog.processors.TimeStamper(fmtiso), structlog.processors.StackInfoRenderer(), structlog.processors.format_exc_info, structlog.processors.UnicodeDecoder(), structlog.processors.JSONRenderer(), # 输出JSON格式 ], context_classdict, logger_factoryLoggerFactory(), wrapper_classstructlog.stdlib.BoundLogger, cache_logger_on_first_useTrue, ) # 获取logger logger structlog.get_logger(llm_service)Prometheus指标metrics.pyfrom prometheus_client import Counter, Histogram, Gauge # 定义指标 llm_requests_total Counter( llm_requests_total, Total number of LLM requests, [model, status] # 标签模型名、状态success/error ) llm_request_duration_seconds Histogram( llm_request_duration_seconds, LLM request duration in seconds, [model] ) llm_tokens_total Counter( llm_tokens_total, Total tokens processed, [model, type] # type: input/output ) # 在客户端中更新指标 def record_metrics(self, model: str, status: str, latency: float, input_tokens: int, output_tokens: int): llm_requests_total.labels(modelmodel, statusstatus).inc() llm_request_duration_seconds.labels(modelmodel).observe(latency) llm_tokens_total.labels(modelmodel, typeinput).inc(input_tokens) llm_tokens_total.labels(modelmodel, typeoutput).inc(output_tokens)Grafana看板关键指标QPS趋势图按模型分组平均延迟热力图X轴时间Y轴模型颜色延迟错误率TOP5模型排行榜Token消耗成本预测按$0.01/1K input tokens, $0.03/1K output tokens计算实操心得在K8s中部署prometheus-operator时必须为LLM服务Pod添加prometheus.io/scrape: true注解否则指标无法采集。我们曾因漏掉这个注解导致连续两周无法监控API健康度。5. 常见问题与排查技巧实录线上故障的终极排查指南5.1 典型问题速查表问题现象可能原因排查命令解决方案httpx.ConnectTimeoutDNS解析失败或防火墙拦截nslookup api.openai.comtelnet api.openai.com 443检查K8s CoreDNS配置在Security Group放行443端口429 Too Many Requests未配置重试或重试策略不合理kubectl logs -l appllm-service | grep 429启用指数退避重试联系OpenAI申请提高配额502 Bad GatewayNginx反向代理超时kubectl exec -it nginx-pod -- cat /var/log/nginx/error.log调大Nginxproxy_read_timeout至30sJSONDecodeError模型返回非JSON内容如HTML错误页curl -v https://api.openai.com/v1/chat/completions检查base_url是否拼写错误确认API Key有效Rate limit exceeded多实例共享同一API Keykubectl top pods --sort-bycpu为每个K8s Namespace分配独立API Key5.2 线上故障排查三板斧第一斧日志溯源使用kubectl logs配合结构化日志过滤# 查看最近10分钟所有429错误 kubectl logs -l appllm-service --since10m \| jq select(.status 429) # 查看特定请求ID的完整链路 kubectl logs -l appllm-service \| grep request_idabc123第二斧网络诊断在Pod内直接测试网络连通性kubectl exec -it pod-name -- sh # 安装诊断工具 apk add curl bind-tools # 测试DNS nslookup api.openai.com # 测试HTTPS连通性 curl -v -H Authorization: Bearer sk-xxx https://api.openai.com/v1/models # 测试HTTP/2支持 curl -v --http2 https://api.openai.com/v1/models第三斧性能压测使用locust模拟真实流量# locustfile.py from locust import HttpUser, task, between import json class OpenAIUser(HttpUser): wait_time between(1, 3) task def chat_completion(self): self.client.post( /v1/chat/completions, json{ model: gpt-4-turbo, messages: [{role: user, content: Hello}], max_tokens: 100, }, headers{Authorization: Bearer sk-xxx} )运行命令locust -f locustfile.py --host https://your-api.com --users 100 --spawn-rate 105.3 独家避坑技巧技巧1环境变量大小写陷阱Linux系统中OPENAI_API_KEY和openai_api_key是不同变量。我们在CI流水线中强制添加检查# .gitlab-ci.yml before_script: - | if [ -z $OPENAI_API_KEY ]; then echo ERROR: OPENAI_API_KEY not set exit 1 fi**技巧2

相关新闻