1. 从零搭建AI工程能力:为什么“会调包”和“能落地”之间隔着一道鸿沟
很多人对AI工程的理解停留在“会调API”这个层面。打开文档,复制一段示例代码,把API Key填进去,跑通了,就觉得自己掌握了AI开发。这种认知在真实项目里会碎得很快。我见过太多团队,Demo阶段一切顺利,一旦进入生产环境,问题就像潮水一样涌出来:模型响应时快时慢、Token消耗远超预算、并发一上来就报错、输出格式飘忽不定、换个模型整个流程就崩了。
这些问题的根源不在于模型本身,而在于AI工程化能力的缺失。所谓AI工程,不是研究模型架构,也不是训练大模型,而是把AI能力可靠地、可观测地、可控制地集成到实际业务系统中的一整套方法论和工具链。它涵盖提示词管理、上下文编排、输出解析、错误重试、成本控制、性能监控、评测体系等多个维度。这些东西在Demo阶段全都被隐藏了,只有在真实流量和真实业务约束下才会暴露。
“ai-engineering-from-scratch”这个主题的核心价值,就是帮助开发者从零开始建立这套工程能力。它适合那些已经会用AI接口、但不知道如何把AI能力做成稳定产品的开发者;也适合那些正在从传统后端转向AI应用层的工程师;还适合技术负责人,需要评估AI项目的工程复杂度和风险点。这篇文章不会教你Transformer的数学原理,也不会带你微调模型,而是聚焦在一件事上:如何把AI能力从“能跑”变成“能扛”。
我自己的经历比较典型。早期做AI应用时,我觉得最难的应该是模型效果,后来才发现,模型效果反而是最容易通过换模型、调提示词来解决的。真正难的是那些“脏活累活”:怎么保证每次请求都能拿到结构化输出、怎么在模型超时时优雅降级、怎么追踪每个用户的Token消耗、怎么在提示词改了之后快速验证没有回归。这些东西没有现成的框架能一键解决,必须自己从工程角度去设计和实现。
接下来的内容,我会按照一个真实项目的演进路径来展开:从最基础的项目结构设计,到提示词工程化管理,再到输出解析与校验,然后是错误处理与重试策略,接着是成本与性能的可观测性,最后是评测体系的搭建。每个环节都会给出具体的代码示例、配置方案和踩坑经验。你可以把这篇文章当作一个AI工程化的路线图,按需取用。
2. 项目骨架设计:别急着写Prompt,先把工程结构搭对
2.1 为什么大多数AI项目的目录结构从一开始就是错的
我见过很多AI项目的代码组织方式是这样的:一个main.py里塞了所有逻辑,提示词以字符串形式硬编码在函数内部,API调用、结果解析、业务处理全部混在一起。这种结构在原型阶段没问题,但一旦要加第二个AI功能、要支持多模型切换、要做A/B测试,就会变成一团乱麻。
正确的做法是从第一天起就把AI工程当作一个独立的层次来对待。我的建议是采用分层架构,至少划分出以下几个模块:
- config层:管理API密钥、模型端点、超时时间、重试次数等配置项,支持环境变量和配置文件两种来源。
- prompts层:存放所有提示词模板,每个提示词独立文件,支持版本管理和变量注入。
- clients层:封装对模型API的调用,统一处理认证、重试、超时、日志。
- parsers层:负责把模型返回的原始文本解析成结构化数据,包含校验和容错逻辑。
- chains层:编排多个AI调用步骤,处理步骤间的数据传递和条件分支。
- observability层:记录每次调用的耗时、Token消耗、成功率、错误类型。
- evals层:存放评测数据集和评测脚本,用于回归测试。
这个结构看起来有点重,但每个模块都有明确的职责边界。比如当你想从GPT-4切换到Claude时,只需要改clients层的实现,上层业务代码完全不用动。当你想调整提示词时,只需要改prompts层的文件,不需要重新部署整个应用。
2.2 配置管理:API Key只是冰山一角
很多开发者对配置管理的理解就是“把API Key放到环境变量里”。这没错,但远远不够。一个生产级的AI应用,配置项至少包括:
| 配置项 | 说明 | 推荐默认值 |
|---|---|---|
model_name | 模型标识 | 根据业务选择 |
temperature | 采样温度 | 0.0-0.3(需要确定性输出时) |
max_tokens | 最大输出长度 | 根据场景设置,避免浪费 |
timeout_seconds | 单次请求超时 | 30秒 |
max_retries | 最大重试次数 | 3次 |
retry_backoff | 重试退避策略 | 指数退避,基数1秒 |
rate_limit_rpm | 每分钟请求上限 | 根据账户等级设置 |
fallback_model | 降级模型 | 更便宜/更快的模型 |
这些配置项应该集中管理,并且支持运行时动态调整。我习惯用一个AIConfig类来承载,从环境变量读取敏感信息,从YAML文件读取业务参数。这样在本地开发、测试环境、生产环境之间切换时,只需要换配置文件,不需要改代码。
注意:不要把任何API Key写入代码或提交到版本库。即使是私有仓库也不建议。用环境变量或密钥管理服务,这是底线。
2.3 提示词模板化:从字符串拼接走向工程化管理
提示词硬编码在代码里是另一个常见问题。这样做有几个坏处:无法独立版本管理、无法做A/B测试、无法非技术人员参与优化、修改后无法快速回滚。
我的做法是把每个提示词存为独立的模板文件,使用类似Jinja2的语法支持变量注入。比如一个商品描述生成的提示词:
你是一个专业的电商文案撰写者。请根据以下商品信息,生成一段吸引人的商品描述。 商品名称:{{ product_name }} 商品类别:{{ category }} 核心卖点:{{ selling_points }} 目标人群:{{ target_audience }} 要求: 1. 描述长度控制在{{ max_length }}字以内 2. 语气{{ tone }} 3. 必须包含所有核心卖点 4. 输出格式为JSON,包含title和description两个字段这样做的好处是,提示词可以独立于代码进行版本控制。每次修改提示词,都可以通过评测体系快速验证效果是否提升。同时,非技术角色(如产品经理、运营)也可以参与提示词的优化,只需要修改模板文件,不需要碰代码。
2.4 客户端封装:统一入口,统一行为
直接在每个业务函数里调用模型API是灾难的开始。你需要一个统一的客户端封装,把所有横切关注点(认证、重试、超时、日志、限流)都收拢到一处。
class AIClient: def __init__(self, config: AIConfig): self.config = config self.session = requests.Session() self.rate_limiter = RateLimiter(config.rate_limit_rpm) def complete(self, prompt: str, **kwargs) -> AIResponse: self.rate_limiter.acquire() start_time = time.time() for attempt in range(self.config.max_retries): try: response = self._call_api(prompt, **kwargs) elapsed = time.time() - start_time self._log_success(prompt, response, elapsed) return response except RetryableError as e: if attempt == self.config.max_retries - 1: self._log_failure(prompt, e) raise time.sleep(self.config.retry_backoff * (2 ** attempt)) except FatalError as e: self._log_failure(prompt, e) raise这个封装的关键点在于:区分可重试错误和不可重试错误。网络超时、限流、服务端5xx错误是可重试的;认证失败、请求格式错误、内容审核不通过是不可重试的。这个区分非常重要,后面讲错误处理时会详细展开。
2.5 一个容易忽略的细节:请求ID与链路追踪
在生产环境里,你一定会遇到“用户说结果不对,但不知道是哪次请求”的情况。所以从第一天起,每次AI调用都应该生成一个唯一的请求ID,并把这个ID贯穿到日志、监控、用户反馈的整个链路中。
我通常会在客户端封装里自动生成一个request_id,格式为{timestamp}-{random_suffix},然后把原始提示词、模型返回、耗时、Token消耗都关联到这个ID上。这样当用户反馈问题时,你可以快速定位到具体的请求记录,复现问题。
这个做法在Demo阶段看起来多余,但在生产环境里能救命。我经历过一次线上事故,用户投诉某个功能输出乱码,因为没有请求ID,排查了整整一个下午才定位到是某个特定输入触发了模型的异常输出。如果有请求ID,五分钟就能定位。
3. 输出解析与校验:让模型输出从“大概对”变成“一定对”
3.1 为什么模型输出总是“差不多但不对”
大语言模型本质上是概率模型,它的输出是采样生成的,不是确定性的。这意味着即使你要求它输出JSON,它也可能给你返回带Markdown代码块包裹的JSON、字段名拼写错误的JSON、或者干脆多了一段解释文字。这不是模型“不听话”,而是它的工作方式决定的。
很多开发者在这个环节的处理方式是“写个正则提取一下”,或者“让模型重新生成一次”。这些方法在简单场景下能用,但在生产环境里不够可靠。你需要一套完整的输出解析与校验机制。
3.2 结构化输出的三种策略及其适用场景
根据对输出格式的严格程度,我把结构化输出策略分为三个层次:
策略一:提示词约束 + 后处理清洗
这是最基础的方式。在提示词里明确要求输出格式,然后在代码里做清洗。比如要求输出JSON,但模型可能返回:
```json {"name": "test", "value": 123}你需要先去掉Markdown代码块标记,再解析JSON。这种方式实现简单,但可靠性一般,适合对格式要求不严格的场景。 **策略二:JSON Mode / 结构化输出API** 现在很多模型提供商都支持强制JSON输出模式。开启后,模型保证返回合法的JSON字符串。这比策略一可靠得多,但仍有局限:它只保证JSON语法合法,不保证字段名和字段类型符合你的预期。 **策略三:Schema校验 + 自动修复** 这是生产环境推荐的方式。在策略二的基础上,增加一层Schema校验。用Pydantic或JSON Schema定义你期望的输出结构,解析后立即校验。如果校验失败,触发自动修复流程:把校验错误信息附加到原始提示词后面,让模型重新生成。 ```python from pydantic import BaseModel, Field, ValidationError class ProductDescription(BaseModel): title: str = Field(max_length=50) description: str = Field(max_length=500) tags: list[str] = Field(min_length=1, max_length=5) def parse_with_repair(raw_output: str, max_repair_attempts: int = 2) -> ProductDescription: for attempt in range(max_repair_attempts + 1): try: data = json.loads(clean_json(raw_output)) return ProductDescription(**data) except (json.JSONDecodeError, ValidationError) as e: if attempt == max_repair_attempts: raise OutputParseError(f"解析失败: {e}") from e repair_prompt = build_repair_prompt(raw_output, str(e)) raw_output = ai_client.complete(repair_prompt).text这个自动修复机制在实际项目里非常有用。根据我的经验,第一次生成就有约85%的概率通过校验,加上一次修复后通过率能到97%以上,两次修复后基本接近100%。
3.3 校验规则的设计:严格但不苛刻
设计校验规则时,要在“严格”和“宽容”之间找到平衡。太严格会导致大量请求触发修复流程,增加成本和延迟;太宽容会让脏数据流入下游系统。
我的经验法则是:
- 必填字段:必须校验,缺失就触发修复。
- 字段类型:必须校验,类型错误通常意味着模型理解偏差。
- 长度限制:设置合理的上下限,但不要过于精确。比如要求标题50字以内,可以放宽到60字再触发修复。
- 枚举值:如果字段是枚举类型,必须校验。但建议在提示词里明确列出所有可选值。
- 格式要求:如邮箱、URL、日期等,用正则校验,但允许一定的格式变体。
提示:校验失败时的错误信息要具体。不要只说“校验失败”,而要告诉模型“字段title缺失”或“字段tags应该是数组但收到了字符串”。具体的错误信息能大幅提高修复成功率。
3.4 处理“模型不配合”的边界情况
有些情况下,模型会持续输出不符合要求的内容。比如你要求输出JSON,但它一直返回自然语言解释。这时候需要设置修复次数上限,超过上限后走降级逻辑。
降级逻辑可以包括:返回默认值、调用备用模型、转人工处理、或者返回一个友好的错误提示。具体选择哪种,取决于业务场景。对于内容生成类场景,返回默认模板可能比报错更合适;对于数据提取类场景,转人工处理可能更稳妥。
我遇到过一个极端案例:某个提示词在特定输入下,模型会持续输出一段固定的拒绝话术。后来发现是提示词里的某个示例触发了模型的安全机制。解决办法是调整示例内容,避免敏感触发词。这个经验告诉我,提示词里的示例和边界情况测试同样重要。
4. 错误处理与重试:区分“值得重试”和“重试也没用”
4.1 AI调用失败的分类学
AI API调用失败的原因五花八门,但可以归为几大类。正确分类是设计重试策略的前提。
| 错误类型 | 典型表现 | 是否可重试 | 处理策略 |
|---|---|---|---|
| 网络超时 | ConnectionTimeout | 是 | 指数退避重试 |
| 服务端错误 | 500, 502, 503 | 是 | 指数退避重试 |
| 限流 | 429 Too Many Requests | 是 | 等待后重试,尊重Retry-After头 |
| 认证失败 | 401 Unauthorized | 否 | 检查密钥配置,立即告警 |
| 请求格式错误 | 400 Bad Request | 否 | 检查请求构造逻辑 |
| 内容审核 | ContentFiltered | 否 | 记录并返回友好提示 |
| 上下文超长 | ContextLengthExceeded | 否 | 截断输入或换用长上下文模型 |
| 模型过载 | ModelOverloaded | 是 | 重试或降级到备用模型 |
这个分类表应该成为你错误处理逻辑的基础。我见过很多项目对所有错误一视同仁地重试,结果认证失败也重试三次,白白浪费时间和配额。
4.2 指数退避与抖动:为什么你的重试总是“撞车”
简单的固定间隔重试在低并发下没问题,但在高并发场景下会导致“重试风暴”:大量请求在同一时刻重试,把服务端压垮。解决办法是使用指数退避加随机抖动。
import random def calculate_backoff(attempt: int, base: float = 1.0, max_backoff: float = 60.0) -> float: exponential = base * (2 ** attempt) jitter = random.uniform(0, exponential * 0.1) return min(exponential + jitter, max_backoff)这个公式的含义是:第一次重试等1秒左右,第二次2秒,第三次4秒,以此类推,同时加入10%的随机抖动。抖动的作用是打散重试时间,避免多个请求同时重试。
4.3 降级策略:当主模型不可用时的Plan B
重试不是万能的。如果主模型持续不可用,你需要一个降级方案。降级策略有几种常见模式:
模式一:切换到备用模型。比如主模型用GPT-4,降级用GPT-3.5。代价是效果可能下降,但至少服务可用。
模式二:返回缓存结果。对于相同或相似的请求,可以返回之前缓存的结果。这需要你有一个语义缓存层。
模式三:返回默认值或模板。对于内容生成类场景,可以返回一个预置的模板,保证功能不中断。
模式四:排队异步处理。把请求放入队列,稍后处理,同时通知用户“处理中”。
选择哪种降级策略,取决于业务对延迟和质量的容忍度。我的建议是在项目早期就设计好降级路径,不要等到线上出问题才临时想方案。
4.4 超时设置:一个容易被忽视的细节
超时时间设置太短,会导致大量正常请求被中断;设置太长,会导致用户等待过久,资源被占用。我的经验值是:
- 同步接口:30秒超时,超过后走降级或异步处理。
- 流式输出:首Token超时10秒,整体超时120秒。
- 批量处理:单条超时60秒,整体超时根据批量大小动态计算。
另外,超时时间应该和重试次数联动考虑。如果单次超时30秒,重试3次,最坏情况下用户要等90秒以上。这在交互式场景里是不可接受的。所以对于交互式场景,我通常设置单次超时15秒,重试1次,总等待控制在30秒以内。
4.5 一个真实的排查案例:间歇性超时
之前遇到过一个诡异的问题:某个AI功能在白天正常,到了晚上高峰期就频繁超时。排查过程如下:
第一步,确认不是代码问题。本地压测没有复现,排除逻辑错误。
第二步,查看监控数据。发现超时集中在晚上8点到10点,且超时请求的输入长度普遍偏长。
第三步,分析模型端指标。发现该时段模型服务的P99延迟从正常的3秒飙升到25秒。
第四步,定位根因。长输入加上高峰期负载,导致模型推理时间大幅增加,超过了我们设置的15秒超时。
解决方案有两个:一是对长输入做预处理,压缩到合理长度;二是对长输入请求单独设置更长的超时时间,并走异步处理路径。这个案例告诉我们,超时设置不能一刀切,要根据输入特征和时段动态调整。
5. 成本与性能可观测性:看不见的消耗最可怕
5.1 Token消耗:AI应用最大的可变成本
AI应用的成本结构和传统应用完全不同。传统应用的成本主要是服务器和带宽,相对固定且可预测。AI应用的成本主要是Token消耗,直接和用户使用量挂钩,波动很大。如果不做监控,很容易出现“月底账单吓一跳”的情况。
Token消耗的监控需要做到几个维度:
- 按用户维度:每个用户消耗了多少Token,用于识别异常用户和做用量限制。
- 按功能维度:每个AI功能消耗了多少Token,用于评估功能ROI。
- 按模型维度:不同模型的Token消耗和成本对比,用于优化模型选择。
- 按时间维度:Token消耗的趋势变化,用于容量规划。
实现方式是在客户端封装里记录每次调用的输入Token数、输出Token数和对应成本,然后上报到监控系统。
@dataclass class TokenUsage: prompt_tokens: int completion_tokens: int total_tokens: int model: str estimated_cost: float def calculate_cost(usage: TokenUsage, pricing: dict) -> float: model_pricing = pricing.get(usage.model, {}) input_cost = usage.prompt_tokens * model_pricing.get("input", 0) / 1000 output_cost = usage.completion_tokens * model_pricing.get("output", 0) / 1000 return input_cost + output_cost5.2 延迟监控:P50不够,要看P95和P99
AI调用的延迟分布通常不是正态分布,而是长尾分布。平均值(P50)看起来很美,但用户体验由长尾决定。一个用户遇到一次10秒的延迟,就会觉得整个产品很慢。
所以延迟监控必须看P95和P99。我的经验是:
- P50应该控制在2秒以内(交互式场景)。
- P95应该控制在5秒以内。
- P99应该控制在10秒以内。
- 超过10秒的请求应该被记录并分析原因。
如果P99持续偏高,通常有几个原因:输入过长、模型过载、网络抖动、重试次数过多。需要逐一排查。
5.3 缓存策略:省钱又提速的利器
AI调用的缓存有两个层次:精确缓存和语义缓存。
精确缓存:对完全相同的输入,直接返回缓存结果。实现简单,用输入文本的哈希值作为Key。适合输入重复率高的场景,比如FAQ问答。
语义缓存:对语义相似的输入,返回缓存结果。需要计算输入的向量表示,然后在向量数据库中查找相似项。实现复杂,但命中率更高。适合用户提问方式多样但意图相似的场景。
class SemanticCache: def __init__(self, embedding_client, vector_store, threshold=0.95): self.embedding_client = embedding_client self.vector_store = vector_store self.threshold = threshold def get(self, query: str) -> str | None: query_embedding = self.embedding_client.embed(query) results = self.vector_store.search(query_embedding, top_k=1) if results and results[0].score >= self.threshold: return results[0].metadata["response"] return None def set(self, query: str, response: str): query_embedding = self.embedding_client.embed(query) self.vector_store.upsert(query_embedding, {"response": response})语义缓存的阈值设置很关键。太高会导致命中率低,太低会导致返回不相关的缓存结果。我的经验是从0.95开始,根据实际效果调整。对于事实性问答,阈值可以设高一些;对于创意生成,缓存的意义不大。
5.4 限流与配额:保护系统也保护钱包
限流有两个目的:保护下游模型服务不被压垮,以及控制成本不超预算。限流策略应该分层设计:
- 全局限流:整个应用对模型API的调用速率上限。
- 用户级限流:每个用户的调用速率上限,防止单个用户占用过多资源。
- 功能级限流:不同AI功能设置不同的限流阈值,核心功能优先保障。
配额管理则是从成本角度出发,给每个用户或每个租户设置Token消耗上限。超过配额后,可以降级到便宜模型、限制功能、或者提示用户升级。
注意:限流和配额的错误提示要友好。不要直接返回“429 Too Many Requests”,而要告诉用户“当前请求较多,请稍后重试”或“本月AI额度已用完,升级可获得更多额度”。
5.5 日志与追踪:出了问题能快速定位
AI应用的日志和传统应用日志有几个不同点:
- 需要记录完整的提示词和模型返回(注意脱敏)。
- 需要记录Token消耗和成本。
- 需要记录请求ID,方便链路追踪。
- 需要记录模型版本和参数配置。
我通常会把每次AI调用记录为一条结构化日志,包含以下字段:
{ "request_id": "20240115-abc123", "timestamp": "2024-01-15T10:30:00Z", "user_id": "user_456", "feature": "product_description", "model": "gpt-4", "prompt_tokens": 350, "completion_tokens": 120, "total_tokens": 470, "estimated_cost": 0.018, "latency_ms": 2340, "status": "success", "retry_count": 0, "cache_hit": false }这些日志既可以用于实时监控,也可以用于离线分析。比如分析哪些功能的成本最高、哪些用户的用量异常、哪些时段的延迟最差。
6. 评测体系:没有评测,就没有迭代
6.1 为什么AI项目比传统项目更需要评测
传统软件的测试是确定性的:输入A,期望输出B,断言相等即可。AI应用的输出是概率性的,同样的输入可能得到不同的输出,而且“好”与“不好”往往没有绝对标准。这使得传统单元测试方法在AI场景下失效。
没有评测体系的AI项目,迭代基本靠感觉。改了提示词,感觉好像好了一点,但不确定是不是真的好了,也不确定有没有在其他场景下变差。这种“盲改”模式在项目初期还能应付,一旦功能复杂起来就会失控。
评测体系的核心价值是:让每次变更都有数据支撑,让迭代方向可衡量、可比较、可回滚。
6.2 评测数据集的构建:从真实场景中来
评测数据集不是凭空造出来的,应该从真实用户请求中采样。我的做法是:
- 冷启动阶段:手动构造20-50条覆盖主要场景的测试用例。
- 上线初期:每天从真实请求中随机采样,人工标注质量,逐步积累到200-500条。
- 稳定阶段:维护一个核心评测集(200条左右),覆盖主要场景和边界情况,每次变更都跑一遍。
评测集的构成应该包括:
- 典型场景:最常见的用户请求,占比60%。
- 边界情况:极端输入、空输入、超长输入,占比20%。
- 困难案例:历史上出过问题的输入,占比20%。
每条评测数据包含输入、期望输出(或评分标准)、以及场景标签。期望输出可以是精确匹配的文本,也可以是一个评分标准(如“必须包含以下要点”)。
6.3 自动评测与人工评测的结合
自动评测适合大规模、快速反馈的场景。常见的自动评测指标包括:
- 精确匹配:输出和期望完全一致,适合分类、提取类任务。
- 包含匹配:输出包含关键信息,适合摘要、问答类任务。
- BLEU/ROUGE:文本相似度,适合翻译、生成类任务。
- LLM-as-Judge:用另一个模型来评分,适合主观质量评估。
人工评测适合小规模、高质量要求的场景。人工评测的维度通常包括:
- 准确性:信息是否正确。
- 完整性:是否覆盖了所有要点。
- 流畅性:语言是否自然。
- 安全性:是否有不当内容。
我的建议是:日常迭代用自动评测快速筛选,重要版本发布前用人工评测做最终把关。两者结合,既保证效率又保证质量。
6.4 LLM-as-Judge:用模型评测模型
LLM-as-Judge是近年来很流行的一种评测方式。用一个能力较强的模型(如GPT-4)来给另一个模型的输出打分。这种方式成本低、速度快,而且和人工评测的相关性在很多场景下能达到80%以上。
实现LLM-as-Judge的关键是设计好评测提示词。提示词需要包含:评分标准、评分维度、参考示例、以及输出格式要求。
JUDGE_PROMPT = """ 你是一个专业的AI输出质量评估员。请根据以下标准对模型的输出进行评分。 用户输入:{input} 模型输出:{output} 期望要点:{expected_points} 评分标准: 1. 准确性(1-5分):输出信息是否正确,有无事实错误。 2. 完整性(1-5分):是否覆盖了所有期望要点。 3. 流畅性(1-5分):语言是否自然、通顺。 4. 安全性(1-5分):是否有不当内容。 请输出JSON格式的评分结果: {{"accuracy": <score>, "completeness": <score>, "fluency": <score>, "safety": <score>, "reason": "<评分理由>"}} """使用LLM-as-Judge时要注意几个问题:评分模型的偏好可能影响结果、评分标准需要反复校准、不同批次的评分可能有漂移。所以建议定期用人工评测校准自动评测的结果。
6.5 回归测试:改了提示词之后,怎么知道没有变差
每次修改提示词或切换模型后,都应该跑一遍回归测试。回归测试的流程是:
- 用评测集跑一遍新版本,记录所有输出和评分。
- 和基线版本对比,计算各项指标的变化。
- 如果关键指标下降超过阈值,阻止发布。
- 如果指标提升或持平,允许发布。
这个流程可以集成到CI/CD中,实现自动化。我通常会在代码仓库里维护一个evals目录,包含评测脚本和基线数据。每次提交涉及提示词或模型配置的变更时,CI会自动触发评测。
# 运行评测 python evals/run_eval.py --prompt-version v2 --baseline v1 --output results/v2_vs_v1.json # 检查结果 python evals/check_regression.py --results results/v2_vs_v1.json --threshold 0.05这个机制看起来有点重,但它是AI项目质量保障的基石。没有它,你永远不知道下一次改动是进步还是退步。
7. 从能跑到能扛:一些踩坑之后的体会
做AI工程这几年,踩过的坑比写过的代码还多。有些教训是通用的,不分具体项目,我想在这里分享几个印象最深的。
第一个体会是:不要相信“模型升级了,问题就解决了”。每次新模型发布,总有人觉得之前的工程问题可以靠换模型解决。但实际情况是,新模型可能在某些方面更强,但在输出格式稳定性、延迟、成本上可能有不同的表现。工程问题需要用工程手段解决,模型升级只是其中一个变量。
第二个体会是:提示词版本管理比想象中重要。我经历过一次线上事故,原因是有人直接在生产环境修改了提示词文件,没有经过评测,导致输出质量大幅下降。后来我们强制要求所有提示词变更必须走代码审查和评测流程,才杜绝了这类问题。
第三个体会是:成本监控要趁早。项目初期流量小,成本不明显,很容易忽视。但一旦流量起来,成本会指数级增长。我建议从第一天起就记录Token消耗,设置预算告警。哪怕一开始只是打印到日志里,也比完全没有强。
第四个体会是:评测集是AI项目最宝贵的资产。代码可以重写,架构可以重构,但一个高质量的评测集需要长时间积累。它记录了你的业务场景、用户需求、质量标准的演变过程。保护好你的评测集,持续维护它,它的价值会随时间增长。
第五个体会是:降级方案不是可选项,是必选项。模型服务不可能100%可用,网络不可能永远稳定。没有降级方案的AI应用,就像没有备份的数据库,迟早会出问题。降级方案可以简单,但必须有。
最后分享一个实用小技巧:在开发阶段,我会在客户端封装里加一个“模拟模式”,可以返回预设的响应,不需要真实调用模型API。这样在写业务逻辑、调试解析代码、跑单元测试时,不需要消耗Token,也不受网络影响。这个模式在团队协作时特别有用,新成员不需要配置API Key就能跑通整个流程。