1. 从"能跑通"到"跑得稳":为什么第十期要聊工程化落地做Python和GPT结合的项目,前九期我们基本把能玩的花样都过了一遍——从最基础的API调用、Prompt设计,到函数调用、流式输出、上下文管理,再到RAG检索增强和简单的Agent编排。说实话,这些东西单拎出来跑个Demo都不难,网上教程一抓一大把。但真正把项目往生产环境一放,问题就全冒出来了:接口偶尔超时、Token消耗失控、并发一上来就报错、日志里全是看不懂的异常堆栈。这一期我想聊的就是这些"脏活累活"。标题叫"秘籍(十)",但内容上它更像是一个分水岭——前十期解决的是"能不能做出来",从这一期开始,我们要解决的是"能不能稳定地跑下去"。如果你已经写过几个能跑的脚本,但一到真实业务场景就心里没底,那这篇内容就是写给你的。我会把我在几个实际项目里踩过的坑、总结的参数配置、以及一套可以直接抄的工程化骨架都摊开讲,尽量做到你照着改改就能用。核心关键词这块,围绕的是Python GPT工程化、API稳定性、Token成本控制、并发处理、异常重试、日志监控这几个方向。这些词听起来有点"运维味",但恰恰是区分"玩具项目"和"可用系统"的关键。我不打算讲太玄的东西,每个点都会落到具体的代码和参数上,让你知道为什么这么设、不这么设会出什么事。2. 工程化骨架的整体设计思路2.1 为什么不能继续用"一个脚本打天下"我见过太多项目,一开始就是一个main.py,里面塞了API调用、Prompt拼接、结果解析、文件读写,两三百行看着还挺清爽。等到要加个重试逻辑、要换模型、要记录每次调用的耗时,代码就开始打架了。改一处崩三处,最后没人敢动。工程化的第一步不是上什么高大上的框架,而是分层。我的习惯是把一个GPT应用拆成四层:配置层、客户端层、业务逻辑层、入口层。配置层管API Key、模型名、超时时间这些易变的东西;客户端层封装对模型接口的调用,统一处理重试、超时、日志;业务逻辑层只关心"我要问什么、拿到结果怎么处理";入口层负责调度和对外暴露。这么拆的好处是,换模型只动客户端层,改Prompt只动业务层,调参数只动配置层。各层之间通过明确的接口通信,谁也不用关心对方内部怎么实现的。听起来像是老生常谈的软件工程原则,但在GPT项目里尤其重要,因为模型接口本身就不稳定,你必须把"不稳定"隔离在一个可控的范围内。2.2 配置管理:别把Key硬编码进代码先说个最基础但最多人犯的错——把API Key直接写在代码里。我见过有人把Key提交到代码仓库,第二天收到账单才发现被人扫走了。正确做法是用环境变量或者配置文件,而且配置文件要加进.gitignore。我一般用pydantic-settings来管理配置,它能自动从环境变量读取,还能做类型校验。比如超时时间你写了个字符串"30",它会直接报错提醒你应该是数字,省得运行时才出问题。配置项我通常分三组:连接相关(base_url、api_key、timeout)、模型相关(model_name、temperature、max_tokens)、行为相关(max_retries、retry_delay、log_level)。这里有个经验:超时时间一定要设,而且要比你预期的长一点。GPT接口在高峰期响应慢是常态,我一般设60秒起步,流式输出的话设120秒。不设超时的话,一个卡住的请求能把整个线程池占满,后面全堵死。2.3 客户端封装:把不稳定性关进笼子客户端层是整个工程化的核心。我的做法是写一个GPTClient类,对外只暴露一个chat方法,内部处理所有脏活。这个类要干几件事:请求重试、超时控制、错误分类、调用日志、Token统计。重试策略上,不是所有错误都值得重试。网络超时、429限流、5xx服务端错误,这些重试有意义;但400参数错误、401鉴权失败,重试一万次也没用,只会浪费时间和配额。所以要先对异常分类,只对"可恢复"的异常重试。重试的间隔我习惯用指数退避,第一次等1秒,第二次2秒,第三次4秒,最多重试3次。这样既能扛住偶发的网络抖动,又不会在服务端真挂了的时候疯狂打请求。日志这块,我建议每次调用都记录:请求时间、模型名、输入Token数、输出Token数、耗时、是否重试、最终状态。这些数据攒起来,你才能回答"这个月成本为什么涨了""哪个环节最慢"这类问题。没有日志的GPT项目,出了问题就是两眼一抹黑。3. 核心细节解析与实操要点3.1 重试机制的正确写法先看一段我实际在用的重试装饰器逻辑,用tenacity库实现,比自己写while循环清爽得多:from tenacity import retry, stop_after_attempt, wait_exponential, retry_if_exception_type import openai @retry( stop=stop_after_attempt(3), wait=wait_exponential(multiplier=1, min=1, max=10), retry=retry_if_exception_type((openai.APITimeoutError, openai.RateLimitError, openai.APIConnectionError)), reraise=True ) def call_with_retry(client, **kwargs): return client.chat.completions.create(**kwargs)这里有几个细节值得说。retry_if_exception_type只捕获那几类可恢复异常,其他异常直接往上抛,不浪费时间。wait_exponential的multiplier=1意味着等待时间是1、2、4、8这样增长,max=10封顶,避免等太久。reraise=True保证重试耗尽后把原始异常抛出来,方便上层处理。注意:重试次数不是越多越好。我试过设5次,结果一个坏请求要等将近一分钟才失败,用户体验极差。3次是个比较平衡的值,覆盖绝大多数偶发问题。还有个坑:流式输出和重试的配合。流式请求一旦开始返回数据,中途断了是很难"续上"的,只能整个重来。所以流式场景下,我一般把重试放在"建立连接"阶段,一旦开始吐字就不再重试,而是把已收到的内容返回给用户,标记为不完整。这个取舍要看业务,如果是聊天场景,用户宁可看到半截回答也不想等重来一遍。3.2 Token成本控制的几个实操手段Token就是钱,这话一点不夸张。我统计过,一个没做优化的项目,光是把完整历史对话每次都塞进上下文,成本能比优化后高出三四倍。控制成本的手段我按性价比排个序:第一,精简System Prompt。很多人写System Prompt跟写作文似的,恨不得把公司简介都塞进去。实际上System Prompt每个请求都要算钱,能砍就砍。我一般控制在200字以内,只保留角色定义和核心约束。第二,历史对话做滑动窗口或摘要。不要无脑保留全部历史。简单做法是只保留最近N轮,复杂点的是把早期对话用模型压缩成一段摘要。我实测下来,保留最近5轮加一段摘要,效果和全量历史差不多,但Token能省60%以上。第三,设置max_tokens上限。不设上限的话,模型可能给你生成一大段废话。根据业务需要设个合理值,比如问答场景设500,写作场景设2000。第四,用便宜模型做预处理。比如意图识别、文本分类这种简单任务,用轻量模型就够了,没必要上最贵的。把任务分级,贵的模型只用在真正需要它的地方。优化手段成本降幅(实测)实现难度对效果影响精简System Prompt10%-20%低极小历史滑动窗口40%-60%中小设置max_tokens15%-30%低可控模型分级调用30%-50%中视任务而定3.3 并发处理:别用多线程硬怼Python的GIL让多线程在CPU密集任务上很鸡肋,但GPT调用是IO密集的,多线程其实能用。不过我更推荐asyncio,因为官方SDK本身就支持异步,配合asyncio.gather能轻松实现并发。import asyncio from openai import AsyncOpenAI async def batch_process(client, prompts): tasks = [client.chat.completions.create( model="gpt-4o-mini", messages=[{"role": "user", "content": p}] ) for p in prompts] return await asyncio.gather(*tasks, return_exceptions=True)return_exceptions=True很关键,它保证某个请求失败不会让整批任务崩掉,失败的会以异常对象的形式返回,你可以单独处理。但并发不是越高越好。我踩过的坑是:一口气发100个请求,结果触发限流,一半失败。后来加了信号量控制并发数:sem = asyncio.Semaphore(10) async def limited_call(client, prompt): async with sem: return await client.chat.completions.create(...)并发数设多少合适?看你的账号等级和接口限流策略。一般从5开始试,逐步往上加,观察有没有429错误。我大部分项目设在10到20之间比较稳。提示:异步代码里千万别混用同步的time.sleep,会把整个事件循环堵死。要等待就用await asyncio.sleep。4. 实操过程与核心环节实现4.1 搭建一个可复用的项目结构我直接给一套我常用的目录结构,你新建项目照着建就行:project/ ├── config/ │ └── settings.py # 配置管理 ├── core/ │ ├── client.py # GPT客户端封装 │ └── exceptions.py # 自定义异常 ├── services/ │ └── chat_service.py # 业务逻辑 ├── utils/ │ ├── logger.py # 日志配置 │ └── token_counter.py # Token统计 ├── main.py # 入口 └── .env # 环境变量(不提交)settings.py用pydantic管理:from pydantic_settings import BaseSettings class Settings(BaseSettings): api_key: str base_url: str = "https://api.openai.com/v1" model_name: str = "gpt-4o-mini" timeout: int = 60 max_retries: int = 3 max_tokens: int = 1000 temperature: float = 0.7 class Config: env_file = ".env" settings = Settings()这样配置集中在一处,改起来方便,而且类型错了会立刻报错。4.2 客户端封装完整实现client.py是重头戏,我把关键部分写出来:import time import logging from openai import OpenAI, APITimeoutError, RateLimitError, APIConnectionError from tenacity import retry, stop_after_attempt, wait_exponential, retry_if_exception_type from config.settings import settings logger = logging.getLogger(__name__) class GPTClient: def __init__(self): self.client = OpenAI( api_key=settings.api_key, base_url=settings.base_url, timeout=settings.timeout ) @retry( stop=stop_after_attempt(settings.max_retries), wait=wait_exponential(multiplier=1, min=1, max=10), retry=retry_if_exception_type((APITimeoutError, RateLimitError, APIConnectionError)), reraise=True ) def chat(self, messages, **kwargs): start = time.time() try: resp = self.client.chat.completions.create( model=kwargs.get("model", settings.model_name), messages=messages, max_tokens=kwargs.get("max_tokens", settings.max_tokens), temperature=kwargs.get("temperature", settings.temperature) ) elapsed = time.time() - start usage = resp.usage logger.info( f"chat success | model={resp.model} | " f"prompt_tokens={usage.prompt_tokens} | " f"completion_tokens={usage.completion_tokens} | " f"elapsed={elapsed:.2f}s" ) return resp.choices[0].message.content except Exception as e: logger.error(f"chat failed | error={type(e).__name__} | msg={str(e)}") raise这段代码把重试、超时、日志、Token统计全包进去了。业务层调用的时候只需要client.chat(messages),完全不用关心底层发生了什么。4.3 业务层的对话管理业务层我一般会写一个ChatService,负责维护对话历史、做窗口裁剪:class ChatService: def __init__(self, client, max_history=5): self.client = client self.max_history = max_history self.history = [] def ask(self, question): self.history.append({"role": "user", "content": question}) # 滑动窗口,只保留最近N轮 trimmed = self.history[-self.max_history * 2:] messages = [{"role": "system", "content": "你是一个简洁的助手。"}] + trimmed answer = self.client.chat(messages) self.history.append({"role": "assistant", "content": answer}) return answermax_history * 2是因为一轮对话包含user和assistant两条消息。这个裁剪逻辑简单但有效,实测能省下大量Token。4.4 参数选择的计算过程temperature和max_tokens这两个参数怎么定,我分享下我的思路。temperature控制随机性,0到2之间。做事实性问答、代码生成,我设0.2到0.3,保证输出稳定;做创意写作、头脑风暴,设0.8到1.0,让模型放开点。默认0.7是个万金油,但别一直用它,该调就调。max_tokens的估算方法是:先跑一批真实请求,统计输出Token的分布,取95分位数再上浮20%作为上限。比如你统计下来95%的回答在400 Token以内,那就设500。这样既不会截断正常回答,又能拦住那些跑偏的长输出。5. 常见问题与排查技巧实录5.1 高频问题速查表问题现象可能原因排查方向解决手段请求频繁超时网络抖动或服务端慢看日志耗时分布加大timeout,加重试429限流并发过高或配额用尽看错误码和调用频率降并发,加信号量输出被截断max_tokens太小看finish_reason调大max_tokens成本异常上涨历史未裁剪或Prompt臃肿统计Token用量滑动窗口,精简Prompt结果不稳定temperature过高对比多次输出降低temperature中文乱码编码问题检查文件编码统一用utf-85.2 几个我踩过的坑坑一:以为重试能解决一切。有次线上大量超时,我第一反应是把重试次数从3加到5,结果更糟——请求堆积,雪崩了。后来才明白,超时的根因是下游服务过载,这时候应该做的是降级和限流,而不是加大重试。重试是应对偶发故障的,不是应对系统性过载的。坑二:日志打太多反而找不到问题。一开始我把每次请求的完整Prompt和响应都打进日志,结果日志文件一天几个G,真出问题的时候翻都翻不动。后来改成只记关键字段:Token数、耗时、状态、错误类型。需要看具体内容的时候,单独开个debug开关。坑三:忽略finish_reason。这个字段告诉你模型为什么停止输出。如果是stop,正常结束;如果是length,说明被max_tokens截断了,回答可能不完整。我有个项目就是因为没看这个字段,用户拿到半截回答还以为模型抽风了。现在我在客户端里会检查这个字段,如果是length就记个警告。坑四:异步里用了同步的库。有次我在异步函数里调了个同步的日志库,结果并发性能还不如单线程。排查了半天才发现是那个库内部有阻塞操作。异步项目里,所有IO操作都要用异步版本,这是铁律。5.3 监控该看哪些指标项目上线后,我每天会看这几个指标:请求成功率(低于99%就要查)、平均耗时(突然升高说明服务端或网络有问题)、Token日均消耗(异常上涨要查是不是有死循环或Prompt膨胀)、重试率(高于5%说明稳定性有问题)。这些指标不用搞什么复杂系统,写个脚本每天定时统计日志就行。提示:给项目设个成本告警。比如日消耗超过预算的80%就发通知。我见过有人因为一个死循环,一晚上烧掉几百块,有告警就能及时止损。6. 关于工程化这件事我的一点体会写了这么多期,我越来越觉得,GPT应用的门槛从来不在"调用接口"这一步,那部分半小时就能学会。真正的门槛在于怎么让它在真实环境里稳定、可控、可维护地跑下去。这一期讲的这些——配置分层、客户端封装、重试策略、成本控制、并发管理、日志监控——单看每一条都不复杂,但组合起来就是一套能扛住生产环境压力的骨架。我自己在实际项目里最大的转变,是从"先跑通再说"变成"先想清楚边界再动手"。比如动手写之前先问自己:这个接口挂了怎么办?成本失控怎么办?并发上来了怎么办?把这些问题的答案提前设计进代码里,比事后打补丁省心得多。这套骨架我用了好几个项目,基本没大改过,顶多是调调参数。你可以直接拿去用,也可以根据自己的场景裁剪,核心思路是通用的:把不稳定的东西隔离起来,把易变的东西集中起来,把重要的东西记录下来。
1. 从"能跑通"到"跑得稳":为什么第十期要聊工程化落地做Python和GPT结合的项目,前九期我们基本把能玩的花样都过了一遍——从最基础的API调用、Prompt设计,到函数调用、流式输出、上下文管理,再到RAG检索增强和简单的Agent编排。说实话,这些东西单拎出来跑个Demo都不难,网上教程一抓一大把。但真正把项目往生产环境一放,问题就全冒出来了:接口偶尔超时、Token消耗失控、并发一上来就报错、日志里全是看不懂的异常堆栈。这一期我想聊的就是这些"脏活累活"。标题叫"秘籍(十)",但内容上它更像是一个分水岭——前十期解决的是"能不能做出来",从这一期开始,我们要解决的是"能不能稳定地跑下去"。如果你已经写过几个能跑的脚本,但一到真实业务场景就心里没底,那这篇内容就是写给你的。我会把我在几个实际项目里踩过的坑、总结的参数配置、以及一套可以直接抄的工程化骨架都摊开讲,尽量做到你照着改改就能用。核心关键词这块,围绕的是Python GPT工程化、API稳定性、Token成本控制、并发处理、异常重试、日志监控这几个方向。这些词听起来有点"运维味",但恰恰是区分"玩具项目"和"可用系统"的关键。我不打算讲太玄的东西,每个点都会落到具体的代码和参数上,让你知道为什么这么设、不这么设会出什么事。2. 工程化骨架的整体设计思路2.1 为什么不能继续用"一个脚本打天下"我见过太多项目,一开始就是一个main.py,里面塞了API调用、Prompt拼接、结果解析、文件读写,两三百行看着还挺清爽。等到要加个重试逻辑、要换模型、要记录每次调用的耗时,代码就开始打架了。改一处崩三处,最后没人敢动。工程化的第一步不是上什么高大上的框架,而是分层。我的习惯是把一个GPT应用拆成四层:配置层、客户端层、业务逻辑层、入口层。配置层管API Key、模型名、超时时间这些易变的东西;客户端层封装对模型接口的调用,统一处理重试、超时、日志;业务逻辑层只关心"我要问什么、拿到结果怎么处理";入口层负责调度和对外暴露。这么拆的好处是,换模型只动客户端层,改Prompt只动业务层,调参数只动配置层。各层之间通过明确的接口通信,谁也不用关心对方内部怎么实现的。听起来像是老生常谈的软件工程原则,但在GPT项目里尤其重要,因为模型接口本身就不稳定,你必须把"不稳定"隔离在一个可控的范围内。2.2 配置管理:别把Key硬编码进代码先说个最基础但最多人犯的错——把API Key直接写在代码里。我见过有人把Key提交到代码仓库,第二天收到账单才发现被人扫走了。正确做法是用环境变量或者配置文件,而且配置文件要加进.gitignore。我一般用pydantic-settings来管理配置,它能自动从环境变量读取,还能做类型校验。比如超时时间你写了个字符串"30",它会直接报错提醒你应该是数字,省得运行时才出问题。配置项我通常分三组:连接相关(base_url、api_key、timeout)、模型相关(model_name、temperature、max_tokens)、行为相关(max_retries、retry_delay、log_level)。这里有个经验:超时时间一定要设,而且要比你预期的长一点。GPT接口在高峰期响应慢是常态,我一般设60秒起步,流式输出的话设120秒。不设超时的话,一个卡住的请求能把整个线程池占满,后面全堵死。2.3 客户端封装:把不稳定性关进笼子客户端层是整个工程化的核心。我的做法是写一个GPTClient类,对外只暴露一个chat方法,内部处理所有脏活。这个类要干几件事:请求重试、超时控制、错误分类、调用日志、Token统计。重试策略上,不是所有错误都值得重试。网络超时、429限流、5xx服务端错误,这些重试有意义;但400参数错误、401鉴权失败,重试一万次也没用,只会浪费时间和配额。所以要先对异常分类,只对"可恢复"的异常重试。重试的间隔我习惯用指数退避,第一次等1秒,第二次2秒,第三次4秒,最多重试3次。这样既能扛住偶发的网络抖动,又不会在服务端真挂了的时候疯狂打请求。日志这块,我建议每次调用都记录:请求时间、模型名、输入Token数、输出Token数、耗时、是否重试、最终状态。这些数据攒起来,你才能回答"这个月成本为什么涨了""哪个环节最慢"这类问题。没有日志的GPT项目,出了问题就是两眼一抹黑。3. 核心细节解析与实操要点3.1 重试机制的正确写法先看一段我实际在用的重试装饰器逻辑,用tenacity库实现,比自己写while循环清爽得多:from tenacity import retry, stop_after_attempt, wait_exponential, retry_if_exception_type import openai @retry( stop=stop_after_attempt(3), wait=wait_exponential(multiplier=1, min=1, max=10), retry=retry_if_exception_type((openai.APITimeoutError, openai.RateLimitError, openai.APIConnectionError)), reraise=True ) def call_with_retry(client, **kwargs): return client.chat.completions.create(**kwargs)这里有几个细节值得说。retry_if_exception_type只捕获那几类可恢复异常,其他异常直接往上抛,不浪费时间。wait_exponential的multiplier=1意味着等待时间是1、2、4、8这样增长,max=10封顶,避免等太久。reraise=True保证重试耗尽后把原始异常抛出来,方便上层处理。注意:重试次数不是越多越好。我试过设5次,结果一个坏请求要等将近一分钟才失败,用户体验极差。3次是个比较平衡的值,覆盖绝大多数偶发问题。还有个坑:流式输出和重试的配合。流式请求一旦开始返回数据,中途断了是很难"续上"的,只能整个重来。所以流式场景下,我一般把重试放在"建立连接"阶段,一旦开始吐字就不再重试,而是把已收到的内容返回给用户,标记为不完整。这个取舍要看业务,如果是聊天场景,用户宁可看到半截回答也不想等重来一遍。3.2 Token成本控制的几个实操手段Token就是钱,这话一点不夸张。我统计过,一个没做优化的项目,光是把完整历史对话每次都塞进上下文,成本能比优化后高出三四倍。控制成本的手段我按性价比排个序:第一,精简System Prompt。很多人写System Prompt跟写作文似的,恨不得把公司简介都塞进去。实际上System Prompt每个请求都要算钱,能砍就砍。我一般控制在200字以内,只保留角色定义和核心约束。第二,历史对话做滑动窗口或摘要。不要无脑保留全部历史。简单做法是只保留最近N轮,复杂点的是把早期对话用模型压缩成一段摘要。我实测下来,保留最近5轮加一段摘要,效果和全量历史差不多,但Token能省60%以上。第三,设置max_tokens上限。不设上限的话,模型可能给你生成一大段废话。根据业务需要设个合理值,比如问答场景设500,写作场景设2000。第四,用便宜模型做预处理。比如意图识别、文本分类这种简单任务,用轻量模型就够了,没必要上最贵的。把任务分级,贵的模型只用在真正需要它的地方。优化手段成本降幅(实测)实现难度对效果影响精简System Prompt10%-20%低极小历史滑动窗口40%-60%中小设置max_tokens15%-30%低可控模型分级调用30%-50%中视任务而定3.3 并发处理:别用多线程硬怼Python的GIL让多线程在CPU密集任务上很鸡肋,但GPT调用是IO密集的,多线程其实能用。不过我更推荐asyncio,因为官方SDK本身就支持异步,配合asyncio.gather能轻松实现并发。import asyncio from openai import AsyncOpenAI async def batch_process(client, prompts): tasks = [client.chat.completions.create( model="gpt-4o-mini", messages=[{"role": "user", "content": p}] ) for p in prompts] return await asyncio.gather(*tasks, return_exceptions=True)return_exceptions=True很关键,它保证某个请求失败不会让整批任务崩掉,失败的会以异常对象的形式返回,你可以单独处理。但并发不是越高越好。我踩过的坑是:一口气发100个请求,结果触发限流,一半失败。后来加了信号量控制并发数:sem = asyncio.Semaphore(10) async def limited_call(client, prompt): async with sem: return await client.chat.completions.create(...)并发数设多少合适?看你的账号等级和接口限流策略。一般从5开始试,逐步往上加,观察有没有429错误。我大部分项目设在10到20之间比较稳。提示:异步代码里千万别混用同步的time.sleep,会把整个事件循环堵死。要等待就用await asyncio.sleep。4. 实操过程与核心环节实现4.1 搭建一个可复用的项目结构我直接给一套我常用的目录结构,你新建项目照着建就行:project/ ├── config/ │ └── settings.py # 配置管理 ├── core/ │ ├── client.py # GPT客户端封装 │ └── exceptions.py # 自定义异常 ├── services/ │ └── chat_service.py # 业务逻辑 ├── utils/ │ ├── logger.py # 日志配置 │ └── token_counter.py # Token统计 ├── main.py # 入口 └── .env # 环境变量(不提交)settings.py用pydantic管理:from pydantic_settings import BaseSettings class Settings(BaseSettings): api_key: str base_url: str = "https://api.openai.com/v1" model_name: str = "gpt-4o-mini" timeout: int = 60 max_retries: int = 3 max_tokens: int = 1000 temperature: float = 0.7 class Config: env_file = ".env" settings = Settings()这样配置集中在一处,改起来方便,而且类型错了会立刻报错。4.2 客户端封装完整实现client.py是重头戏,我把关键部分写出来:import time import logging from openai import OpenAI, APITimeoutError, RateLimitError, APIConnectionError from tenacity import retry, stop_after_attempt, wait_exponential, retry_if_exception_type from config.settings import settings logger = logging.getLogger(__name__) class GPTClient: def __init__(self): self.client = OpenAI( api_key=settings.api_key, base_url=settings.base_url, timeout=settings.timeout ) @retry( stop=stop_after_attempt(settings.max_retries), wait=wait_exponential(multiplier=1, min=1, max=10), retry=retry_if_exception_type((APITimeoutError, RateLimitError, APIConnectionError)), reraise=True ) def chat(self, messages, **kwargs): start = time.time() try: resp = self.client.chat.completions.create( model=kwargs.get("model", settings.model_name), messages=messages, max_tokens=kwargs.get("max_tokens", settings.max_tokens), temperature=kwargs.get("temperature", settings.temperature) ) elapsed = time.time() - start usage = resp.usage logger.info( f"chat success | model={resp.model} | " f"prompt_tokens={usage.prompt_tokens} | " f"completion_tokens={usage.completion_tokens} | " f"elapsed={elapsed:.2f}s" ) return resp.choices[0].message.content except Exception as e: logger.error(f"chat failed | error={type(e).__name__} | msg={str(e)}") raise这段代码把重试、超时、日志、Token统计全包进去了。业务层调用的时候只需要client.chat(messages),完全不用关心底层发生了什么。4.3 业务层的对话管理业务层我一般会写一个ChatService,负责维护对话历史、做窗口裁剪:class ChatService: def __init__(self, client, max_history=5): self.client = client self.max_history = max_history self.history = [] def ask(self, question): self.history.append({"role": "user", "content": question}) # 滑动窗口,只保留最近N轮 trimmed = self.history[-self.max_history * 2:] messages = [{"role": "system", "content": "你是一个简洁的助手。"}] + trimmed answer = self.client.chat(messages) self.history.append({"role": "assistant", "content": answer}) return answermax_history * 2是因为一轮对话包含user和assistant两条消息。这个裁剪逻辑简单但有效,实测能省下大量Token。4.4 参数选择的计算过程temperature和max_tokens这两个参数怎么定,我分享下我的思路。temperature控制随机性,0到2之间。做事实性问答、代码生成,我设0.2到0.3,保证输出稳定;做创意写作、头脑风暴,设0.8到1.0,让模型放开点。默认0.7是个万金油,但别一直用它,该调就调。max_tokens的估算方法是:先跑一批真实请求,统计输出Token的分布,取95分位数再上浮20%作为上限。比如你统计下来95%的回答在400 Token以内,那就设500。这样既不会截断正常回答,又能拦住那些跑偏的长输出。5. 常见问题与排查技巧实录5.1 高频问题速查表问题现象可能原因排查方向解决手段请求频繁超时网络抖动或服务端慢看日志耗时分布加大timeout,加重试429限流并发过高或配额用尽看错误码和调用频率降并发,加信号量输出被截断max_tokens太小看finish_reason调大max_tokens成本异常上涨历史未裁剪或Prompt臃肿统计Token用量滑动窗口,精简Prompt结果不稳定temperature过高对比多次输出降低temperature中文乱码编码问题检查文件编码统一用utf-85.2 几个我踩过的坑坑一:以为重试能解决一切。有次线上大量超时,我第一反应是把重试次数从3加到5,结果更糟——请求堆积,雪崩了。后来才明白,超时的根因是下游服务过载,这时候应该做的是降级和限流,而不是加大重试。重试是应对偶发故障的,不是应对系统性过载的。坑二:日志打太多反而找不到问题。一开始我把每次请求的完整Prompt和响应都打进日志,结果日志文件一天几个G,真出问题的时候翻都翻不动。后来改成只记关键字段:Token数、耗时、状态、错误类型。需要看具体内容的时候,单独开个debug开关。坑三:忽略finish_reason。这个字段告诉你模型为什么停止输出。如果是stop,正常结束;如果是length,说明被max_tokens截断了,回答可能不完整。我有个项目就是因为没看这个字段,用户拿到半截回答还以为模型抽风了。现在我在客户端里会检查这个字段,如果是length就记个警告。坑四:异步里用了同步的库。有次我在异步函数里调了个同步的日志库,结果并发性能还不如单线程。排查了半天才发现是那个库内部有阻塞操作。异步项目里,所有IO操作都要用异步版本,这是铁律。5.3 监控该看哪些指标项目上线后,我每天会看这几个指标:请求成功率(低于99%就要查)、平均耗时(突然升高说明服务端或网络有问题)、Token日均消耗(异常上涨要查是不是有死循环或Prompt膨胀)、重试率(高于5%说明稳定性有问题)。这些指标不用搞什么复杂系统,写个脚本每天定时统计日志就行。提示:给项目设个成本告警。比如日消耗超过预算的80%就发通知。我见过有人因为一个死循环,一晚上烧掉几百块,有告警就能及时止损。6. 关于工程化这件事我的一点体会写了这么多期,我越来越觉得,GPT应用的门槛从来不在"调用接口"这一步,那部分半小时就能学会。真正的门槛在于怎么让它在真实环境里稳定、可控、可维护地跑下去。这一期讲的这些——配置分层、客户端封装、重试策略、成本控制、并发管理、日志监控——单看每一条都不复杂,但组合起来就是一套能扛住生产环境压力的骨架。我自己在实际项目里最大的转变,是从"先跑通再说"变成"先想清楚边界再动手"。比如动手写之前先问自己:这个接口挂了怎么办?成本失控怎么办?并发上来了怎么办?把这些问题的答案提前设计进代码里,比事后打补丁省心得多。这套骨架我用了好几个项目,基本没大改过,顶多是调调参数。你可以直接拿去用,也可以根据自己的场景裁剪,核心思路是通用的:把不稳定的东西隔离起来,把易变的东西集中起来,把重要的东西记录下来。