跳到主要内容

780 篇博文 含有标签「ai-engineering」

查看所有标签

那个没人同意却成了规范的评估套件

· 阅读需 9 分钟
Tian Pan
Software Engineer

打开任何成熟的智能体(agent)代码库,问一个简单的问题:需求文档在哪里?不是融资演示文稿,不是发布文档,也不是那个上次更新还在第三季度的 Notion 页面。那份具体且明确地规定了这个智能体应该做什么的产出物在哪里?

对于大多数团队来说,诚实的回答是:评测套件(eval suite)。那里有一个测试用例文件夹——输入与预期输出成对出现,还有评分标准、评判提示词——以及一个显示通过或失败的 CI 门禁。那个文件夹是唯一一个将“正确”定义得足够精确以供执行的地方。其他一切都是散文,而散文会随时间发生偏移。

这本身并不坏。一个可执行的规范比没人读的 PRD 更诚实。问题在于,几乎没有人将评测套件视为规范。它是由一名工程师在截止日期前拼凑出来的,只是为了让发布门禁显示为绿色。它编码了一百个从未被记录、从未被审查、也从未被达成共识的判断。而模型现在正针对它进行精确优化。

那个你从未进行过负载测试的备用模型

· 阅读需 10 分钟
Tian Pan
Software Engineer

每一个具有韧性的 LLM 设计在配置中都有一行代码指定了一个备选模型。它之所以存在,是因为有人在设计评审中提出了正确的问题——“如果主模型宕机了怎么办?”——而另一个人用一个 fallback: 键回答了这个问题。大家都点头表示赞同。架构图上多了一个带有虚线箭头的第二个框。合规文档中也增加了一句关于优雅降级的描述。

然后,再也没有人碰过它。

备选模型是大多数生产环境 AI 系统中被断言得最自信、但演练最少的组件。它被命名、被记录、被画在图中——而在它真正承载流量的那一天,也是它第一次遇到真实请求的一天。你并没有建立安全网。你只是构建了第二个断裂强度未知的模型,而你将在最糟糕的时刻发现那个极限。

你的备用路径是生产环境中唯一未经测试的代码

· 阅读需 10 分钟
Tian Pan
Software Engineer

每个成熟的 AI 系统都会配备回退方案(fallback)。当主模型被限流时,路由到更廉价的模型。当服务商返回 5xx 错误时,提供缓存的答案。当置信度低于阈值时,回退到手写的启发式规则(heuristic)。架构图中有一个清晰的小分支,标注为“降级模式”(degraded mode),每个人都因此感到更安全。

令人不安的部分在于:那个分支是系统中几乎从未运行过的唯一代码。主路径每天执行数百万次,并经过大量流量的调试、性能分析和实战测试。回退路径几乎从不执行——直到某天,它在负载下、在事故期间、在三名工程师看着仪表盘变红时,突然为所有人同时执行。

一个你不练习的回退方案并不是冗余。它是一个不受监控的第二系统,其“首秀”在统计学上注定会发生在最糟糕的时刻。

LLM 裁判是一个带版本的依赖,而非中立的基础设施

· 阅读需 10 分钟
Tian Pan
Software Engineer

大多数团队对待 LLM 评审员(LLM judge)的方式就像对待单元测试运行器一样:将其视为产生可信数字的中性基础设施。你编写评分标准(rubric),让模型针对你的输出进行评估,然后评审员返回分数。分数会显示在仪表盘上。仪表盘的趋势线驱动着产品路线图(roadmap)。没有人认为评审员是一个具有“行为”的东西,因为自动化的全部意义就在于将人为行为从环节中剔除。

但评审员本质上是一个模型。它有版本,有偏差。一旦它发生变化——无论是评估平台团队为了省钱更换了模型,还是提供商在 -latest 别名后悄悄滚动了权重——它产生的所有历史分数与新分数之间都会变得不可比。你的季度质量趋势现在是用两种不同的货币计价的,而且没有人给出汇率。

这并非假设的边缘情况。如果不像对待测量仪器那样对 LLM 进行版本化管理,这就是将其作为测量工具的必然结果。

自信地返回错误答案的语义缓存

· 阅读需 11 分钟
Tian Pan
Software Engineer

两个支持用户在短短一分钟内向你的智能体提出了几乎相同的问题。第一个用户问:“我们针对 EU 订单的退款期限是多久?”第二个用户问:“我们针对 US 订单的退款期限是多久?”这两个句子的嵌入向量(embeddings)仅有一丝之差——长度相同,结构相同,只有一个两字母 token 的区别。你的语义缓存经过了相似度阈值的微调(在演示中看起来非常合理),将它们判定为匹配。于是,第二个用户得到了第一个用户的答案。EU 的 14 天冷静期被当作事实,以流利的文字呈现给了一位 US 客户,且没有任何补充说明。

没有人会因此收到报警。缓存返回了 200。延迟表现优异。成本看板显示了一次命中,这正是每个人想要的结果。唯一能说明出了问题的信号是客户根据一项不适用于他们的政策行事——而这个信号在几天后才会通过退款纠纷传来,而不是通过你的监控系统。

正是这种故障模式,让语义缓存与你之前构建过的任何缓存都截然不同。精确匹配缓存可能会过期,但它永远不会 错误——key 要么匹配,要么不匹配。语义缓存则是有意放弃了这种保证。它被设计为能针对从未见过的 key 返回答案,而这种延迟优势的代价是正确性风险,大多数团队从未对这一风险进行过量化。

用户过早行动的流式 Token

· 阅读需 9 分钟
Tian Pan
Software Engineer

一个用户询问你的助手某项配置更改是否可以安全发布。模型流式返回了:“是的,你可以安全地部署。” 300 毫秒后,它继续写道:“——但在 us-east 区域除外,那里的旧连接池仍在排空。”但用户已经读完了前半部分,感受到了绿灯带来的放松,并点击了部署。这句修正说明到达时,人已经走开了。

这里没有人犯错。模型是正确的。用户阅读了屏幕上的内容。渲染器忠实地显示了每个到达的 Token。然而结果却是一次糟糕的部署,因为流式传输将模型的“中间状态”变成了用户视为“最终结果”的东西。

结构化输出并非经过验证的输出

· 阅读需 10 分钟
Tian Pan
Software Engineer

你的团队启用模式约束解码(schema-constrained decoding)的那一天感觉像是一个里程碑。解析错误停止了。JSONDecodeError 警报消失了。从文本中抓取字段的脆弱正则表达式也被删除了。有人在站会上说“模型现在返回有效的 JSON 了”,结构化输出的任务单随之关闭。

那句话正是麻烦的开始。“模型现在返回有效的 JSON 了”是正确性工作的开始,而不是结束。JSON 模式和约束解码保证了响应的形状(shape)——即 quantity 是一个整数,status 是三个枚举值之一,对象包含你要求的键。它们完全无法保证 quantity 是否是正确的数字,status 是否反映了真实发生的情况,或者 sku 字段是否指向了目录中存在的商品。

那个由智能体编写的、实际上什么也没测的测试

· 阅读需 11 分钟
Tian Pan
Software Engineer

让一个编程智能体 (AI agent) “为这个模块添加测试”,你会得到测试。它们格式整齐,遵循你的项目规范,而且能够通过。覆盖率会上升。这个 PR 看起来非常尽职。然而,这些测试中很大一部分根本无法捕捉到你可能引入的任何 Bug。

这并不是一个关于模型太蠢的故事。智能体完全按照要求完成了任务。问题在于,“添加测试”和“添加能约束行为的测试”是不同的请求,而其中只有一个是能被一眼验证的。无论是真正的断言还是同义反复(tautology),绿色的对勾看起来都一模一样。

结果就是,测试套件的代码行数在增加,但效能却在萎缩。你最终得到了更多的文件、更多的 CI 耗时、更多的维护成本——而交付回归缺陷的概率却与开始前几乎无异。

悄然失效的评估:当你的测试套件在衡量一个已不存在的世界

· 阅读需 11 分钟
Tian Pan
Software Engineer

你的评测套件通过了。240 个案例全是绿色,和上周一样。你发布了代码。两天后,支持工单激增。当你阅读对话记录时,你发现了一种你的套件完全没有立场的失败模式——不是某个案例从通过变成了失败,而是用户开始问一些你的套件从未想到过要去问的问题。

这就是评测(evals)的无声失败。我们将“全绿”视作对现状的肯定:“系统运行正常”。实际上,它只是对过去的一种陈述——即编写这些评测案例的那一刻。六个月前编写的评测编码了当时的三样东西:产品的范围、模型的失败模式,以及真实用户表达请求的方式。而这三者都在变化。功能增加了新的界面,模型升级了两次。随着用户了解产品的功能,输入分布也发生了漂移。套件没有随之移动,因此全绿的运行结果越来越只是在证明一个不再存在的世界。

没有人注意到,因为没有东西崩溃。过时的评测不会报错。它会继续自信地通过,但衡量的关键内容却越来越少。

你的工具描述是模型遵循的指令通道

· 阅读需 9 分钟
Tian Pan
Software Engineer

当安全团队审查一个新的工具集成时,他们会阅读代码。他们会检查函数的功能、它触及的内容、它需要的权限范围(scopes),以及它是否记录了敏感秘密。但他们几乎从不阅读那句决定模型是否调用该工具的句子——工具描述。那句话不仅仅是文档。它是模型视为权威的指令,而在大多数智能体堆栈中,没有人会去审计它。

工具描述是写给模型看的。模型利用它来决定工具何时相关、应该传递哪些参数,以及如何解释返回的结果。这使得描述成为了进入模型行为的一个控制通道。而当一个工具来自第三方注册表、一个你不运行的模型上下文协议(MCP)服务端,或者一个同事上周安装的插件时,这个控制通道的作者就是你从未同意信任的人。

这就是差距所在。输入净化(Input sanitization)检查用户输入的内容。代码审计(Code review)检查函数执行的内容。工具描述介于两者之间——它是表现得像输入的配置——它从这两个防护网中漏掉了。

当两个 Agent 共享一个工具:多 Agent 系统中的并发 Bug

· 阅读需 11 分钟
Tian Pan
Software Engineer

当你输入“启动另一个智能体来并行处理”的那一刻,你就已经成为了一名分布式系统工程师。你可能没有注意到。框架让它变成了一行代码的改动,演示运行良好,延迟也降低了。但在底层,你刚刚引入了两个在没有协调的情况下读写共享状态的进程 —— 而困扰了数据库领域五十年的每一个竞态条件 (race condition)、更新丢失 (lost update) 和脏读 (dirty read),现在都潜伏在你的智能体堆栈中,伺机而动。

这种情况之所以棘手,是因为故障看起来不像并发 Bug,而像是某个智能体出错了。输出在语法上是有效的,流水线显示绿色,没有抛出异常 —— 然而,客户被收取了两次费用,或者文件丢失了一半预期的内容,又或者一个智能体自信地根据另一个智能体已经覆盖的数字采取了行动。你去调试那个“愚蠢的智能体”,发现它的提示词 (prompt) 没有任何问题,因为提示词根本就不是问题的症结所在。

你的向量索引是一个没有失效策略的缓存

· 阅读需 11 分钟
Tian Pan
Software Engineer

向量索引感觉就像一个数据库。你向其中写入文档,查询它,它返回结果。但它并不是数据库——它是存储在其他地方的数据的“派生、非规范化副本”。你的事实来源是 wiki、工单系统、CRM 或 PDF 文件夹。嵌入 (embeddings) 是这些事实的投影,冻结在你运行摄取任务 (ingestion job) 的那一刻。

这使得你的向量索引变成了一个缓存。就像所有缓存一样,它会失效。不同之处在于,大多数团队是有意识地构建缓存层,带有 TTL 和失效钩子 (invalidation hook),而几乎没有人会有意识地将向量索引作为缓存来构建。他们将其构建为“知识库”,然后在它提供三周前就已经过时的知识时感到惊讶。