跳到主要内容

720 篇博文 含有标签「llm」

查看所有标签

隐藏的 SDK 重试机制:为什么你付了两倍的钱却浑然不知

· 阅读需 12 分钟
Tian Pan
Software Engineer

打开 OpenAI Python SDK 的源代码,你会发现一行安静的代码:DEFAULT_MAX_RETRIES = 2。Anthropic SDK 也采用了同样的默认设置。大多数 TypeScript SDK 也是如此。两次重试,指数级退避(exponential backoff),在连接错误、408、409、429 以及任何 5xx 错误时自动触发——这些都在你的代码看到失败之前就执行了。你没有配置它。你没有选择加入。你通常甚至不知道它的发生,因为你的应用记录的指标是 request_count(请求数),而不是 attempt_count(尝试数),并且你的追踪器(tracer)唯一能看到的 span 是 SDK 在最后一次尝试后关闭的最外层 span。

这在大多数情况下都没问题,直到出问题为止。如果在该 SDK 调用之上再添加一个应用级的重试装饰器——那种每个团队在遇到第一个 429 错误后都会写的代码——你就构建了一个 3x3 的风暴:SDK 尝试三次,你的包装层围绕 SDK 又尝试三次,在服务商降级期间,一个单一的用户请求会扇出为九次推理调用。服务商的账单会计算每一次尝试。而你的仪表盘只记录了一次。当最终有人进行账实对账时,那将是一场谁都不会喜欢的季度末谈话。

负面提示词是代码异味:为什么系统提示词中的每个 “不要” 都是技术债

· 阅读需 11 分钟
Tian Pan
Software Engineer

打开任何已经上线超过三个月的生产环境 AI 功能的系统提示词(system prompt)。数一数其中的负向条款——“不要”、“绝不说”、“避免”、“在任何情况下都不”、“你绝不能”。如果计数达到了两位数,你看到的就不是一个系统提示词。你看到的是一个坟场。每一块墓碑都标志着一个特定的用户投诉、一个特定的事故报告,或者一条来自利益相关者的 Slack 消息,因为他们看到模型做出了一些令人尴尬的事情。团队在表面打了补丁后就继续前进了,现在这个提示词读起来就像一份被强行嫁接了人格的法律免责声明。

负向提示词是代码异味(code smells)。并非隐喻意义上的,而是字面意义上的。它们在提示词工程中相当于吞掉异常的 try/except 块、没有文档的配置标志,或者是 2022 年留下的 // TODO: refactor this。它们在某种程度上有效,直到它们失效。而且它们所掩盖的失败模式,几乎总是比它们被添加用来压制的那个失败更有趣。

策略文件:为什么你不应该把拒绝规则写在系统提示词里

· 阅读需 13 分钟
Tian Pan
Software Engineer

上个季度,一家金融科技初创公司的安全审核员在系统提示词(system prompt)中添加了四行内容。这次修改包含一条拒绝规则,旨在防止助手为公司未获得运营许可的司法管辖区提供具体的税务建议。这听起来很合理、范围明确且符合审计要求。该规则在周二上线。到周五时,评估套件显示在与税务完全无关的客户入职流程中出现了 7 个点的下降——模型开始对任何提及国家的提问都模棱两可,甚至包括“这个账户持有哪种货币”。产品团队撤回了修改。安全团队在下周以略有不同的措辞重新发布了它。三周后,同样的退化以不同的形式再次出现,而接下来的安全修改又破坏了另一个无关的流程。

这里的 bug 不在于措辞,而在于拒绝规则放错了位置。它被挤进了一个 2,400 token 的构件中,该构件还包含助手的对话语气、格式契约、任务指令以及其他六项策略条款——对其中任何一项的修改都是对所有内容的行为修改,因为模型无法分辨哪句话是策略,哪句话是风格。生产环境中的系统提示词之所以变成了一坨乱麻的单体,是因为三个正交的关注点伪装成了一个整体。没有将它们解耦的团队在每次修改时都在支付“集成税”。

Prompt 修改不只是措辞变动:将 Prompt 视为软件的代码审查规范

· 阅读需 13 分钟
Tian Pan
Software Engineer

周二下午,一个只有六行代码的系统提示词(system prompt)编辑出现在了一个 Pull Request (PR) 中。Diff 只是普通的英文。两位评审者扫了一眼新的措辞,觉得读起来更自然,于是点击了批准。PR 在不到一分钟内合并。到了周五,客服开始收到关于智能体的工单:它突然拒绝总结超过一定长度的文档,不再引用来源,并莫名其妙地在每句回复开头都加上 “Certainly!” —— 这种行为没人要求过,Diff 中也无法预见。

当一个花了十年时间学习如何评审代码的团队,在面对提示词这一产物时,竟然退化到了第一周的水平,结果就是这样。Diff 看起来 毫无害处,因为它读起来像英语,而人类正是用眼睛来审阅英语的。让代码评审发挥作用的规范 —— 运行测试、检查影响范围、对 “小改动” 保持适当的怀疑 —— 并没有悄然转化。措辞变好了,但行为变差了,直到用户发现之前,没人注意到。

RAG 中的新鲜度与相关度权衡:为什么你无法在查询时同时优化两者

· 阅读需 13 分钟
Tian Pan
Software Engineer

一名用户询问你的助手公司的育儿假政策。机器人返回了 12 周,并附带了引用。被引用的文档是 2023 年的正确答案;而人力资源部门在上个季度发布了更新,将其延长到了 16 周。这两个版本都在你的知识库中。由于旧版页面的表述更简洁且模棱两可的内容较少,余弦相似度给 2023 年版本的评分是 0.87,而 2024 年版本的评分是 0.84。较新的文档以 3 个百分点的差距落败,用户得到了一个看似经过审计的错误答案。

这就是时效性与相关性的权衡(freshness-relevance tradeoff),令人不安的是,这在查询时并没有完美的解决方案。如果你增加时效性的权重,检索结果就会偏向于昨天刚编辑过的任何内容——在大多数知识库中,这些通常是高频变动的嘈杂区域,不应作为事实来源。如果你不增加时效性的权重,你给出的答案将基于几个月前就被取代的文档。没有一个全局按钮能同时搞定这两点,大多数团队只有在一些令人尴尬的答案绕过评估套件泄露出去后,才会发现这个问题。

检索级联失效:文档删除如何毒害你的 RAG 流水线

· 阅读需 11 分钟
Tian Pan
Software Engineer

一个用户询问你的支持机器人退款期限何时结束。机器人带着愉快的自信给出了“60 天”的回答并附带了引用。然而,那个写着“60 天”的策略页面早在三个月前就从 CMS 中删除了。新策略是 14 天。直到有客户投诉,你的团队中才有人意识到机器人出错了。

这就是检索级联失效(retrieval cascade failure):文档已从事实源中消失,但其嵌入(embedding)仍留在索引中,在余弦相似度排名中依然靠前,不断为模型提供一个“幽灵”。RAG 流水线将向量索引视为源内容的缓存,但大多数团队在构建缓存时并没有构建失效机制。插入操作得到了所有的工程关注,而删除操作只得到了一个 TODO 注释。

“展示过程”的 UX 陷阱:当推理链只是披着产品外壳的调试输出

· 阅读需 11 分钟
Tian Pan
Software Engineer

推理模型会输出思维链(chain-of-thought)轨迹,因为这是它的计算方式。产品团队在 UI 中渲染该轨迹,是因为隐藏它感觉像是丢掉了用户付费购买的 token。这是两个不同的决定,而产品端几乎没有人意识到他们做了第二个决定。于是,轨迹变成了面板,面板变成了功能,功能有了文档页面。六个月后,有人在季度回顾中问,为什么支持队列里全是用户在反驳推理过程,而不是针对答案本身。

推理轨迹本质上是调试输出。它的存在是为了让工程师了解模型为什么选择某个工具、在日期上含糊其辞,或者在段落中间悄悄切换了角色。在没有经过设计审查的情况下将其推给终端用户,等同于在生产环境中留下 console.log 调用并称之为“透明度”。它看起来像个功能,渲染成本几乎为零,但它会以团队构建的任何仪表盘都无法显示的方式悄悄削弱信任。

小模型,大账单:为什么单 Token 成本更低反而更贵

· 阅读需 10 分钟
Tian Pan
Software Engineer

由财务主导的“切换到更小模型”的指令,是让你的 LLM 账单季度环比增长最可靠的方式之一。采购团队盯着的仪表盘——单次调用成本、每次请求的平均 token 数——一直在下降。与此同时,发票金额却在不断攀升。当有人终于把这两者对上账时,团队已经花了六个月的时间进行提示词(prompt)迭代,以补偿那个在任务处理上表现更差的模型,而且团队已经陷得太深,如果不承认最初的切换是个错误,就无法走回头路。

错误不在于定价,而在于计量单位。当推理深度、重试次数和提示词大小都随模型而异时,单 token 价格是一个具有误导性的维度。正确的指标是“单次成功完成所需的 token 数”,在这个维度上,更便宜的模型往往会输。

停止序列的“自毁”陷阱:当用户输入与分隔符发生冲突

· 阅读需 12 分钟
Tian Pan
Software Engineer

一位用户将一段 Markdown 粘贴到你的支持代理中。他们粘贴内容中的第一个标题是 ### Steps I tried。你的提示词模板(prompt template)使用 ### 作为停止序列(stop sequence)。模型尽职地读取了用户的输入,开始回答,并生成了 ### 作为其结构化响应的一部分——结果 API 返回了两句自信的回复,随后便是沉默。工单以“模型质量退化”的名义进入你的队列。其实不然。修复方法只是网关中的一行代码。

停止序列是生产级 LLM 技术栈中极其关键却又常被忽视的调节开关。它们通常是在最初编写提示词的那一周选定的,那时输入还是整洁的工程示例,还没有人粘贴过 JIRA 工单的堆栈信息。十二个月后,用户内容的分布已经远远超出了提示词作者的想象,曾经整洁的分隔符现在变成了潜伏在每三百个用户粘贴中就有一个的隐患。没有任何告警。评估套件(eval suite)依然能够通过。受影响部分的 CSAT 指标下降了 0.5 分并维持在那里。

这不是模型的问题。这是一个伪装成模型问题的输入契约(input-contract)问题,它的形态类似于典型的分布式系统 Bug:为一方的内容分布选择的分隔符被强制应用于另一方的内容分布,且在边界处没有任何监控。

流式结构化输出:为什么你的解析器会在第 47 个 Token 处卡住

· 阅读需 12 分钟
Tian Pan
Software Engineer

团队第一次构建带有结构化输出的流式 AI 功能时,遇到的 bug 总是如出一辙。模型生成正常,数据块(chunks)接收正常。但在第 47 个 token 左右,解析器挂掉了,UI 冻结了,或者更糟——一个半成型的枚举(enum)值被路由到了下游工具,导致其悄无声息地执行了错误操作。团队在 JSON.parse 周围加了一个 try/catch,觉得自己搞定了,然后发布。两周后,兄弟团队抱怨响应变长后流式 UI 感觉很卡。一个季度后,事故审查询问为什么在一个模型仍在描述为 "DeleteIfEmpty" 的记录上触发了 "Delete" 工具调用。

Bug 不在任何单个 token 中。Bug 在于 token 流式传输和结构化输出在架构上是冲突的,而大多数框架只是用“祈祷”来掩盖这种冲突。Schema 说“这是一个完整的对象”。Token 流说“这是一次一个字节的数据”。从定义上讲,这两个端点之间的每一个中间状态对于 Schema 来说都是无效的。团队的工作是决定在这些中间状态期间该做什么——而大多数团队并没有明确做出这个决定。

总结税:当压缩消耗的 Token 超过了它节省的量

· 阅读需 12 分钟
Tian Pan
Software Engineer

一个长时间运行的 Agent 每 12 轮就会达到其压缩阈值。每一次压缩的成本都相当于一次规模等同于当前运行窗口的 LLM 调用——首先是 8K tokens,然后是 14K,接着是 22K——因为被总结的内容跨度随着每次触发而增长。到了第 60 轮,用户在观察 Agent 自我总结上花费的 token,已经超过了在实际推理上花费的 token。成本仪表盘将“用户推理成本”显示为一个单一数字,完全没有意识到其中一半是为用户永远不会再看的上下文压缩而付费的。

这就是“总结税”:一类随着对话长度增加而增长的开销,在用户回合之间悄然触发,并作为一个单一项目出现,将用户付费的工作与系统为自我管理而进行的内务处理混为一谈。这是现代 Agent 架构中最接近“垃圾回收停顿时间(garbage-collection pause time)”的东西——而大多数团队在生产环境中运行时都关闭了 -verbose:gc

每个开放 RAG 系统自带的攻击向量

· 阅读需 11 分钟
Tian Pan
Software Engineer

五份精心设计的文档。260 万条记录的语料库。操纵特定 AI 响应的成功率高达 97%。这是来自 PoisonedRAG 的基准测试结果,该研究发表于 USENIX Security 2025 —— 而且这种攻击不需要模型访问权限,不需要在推理阶段进行提示词注入,甚至不需要与系统进行任何直接交互。攻击者只需向知识库贡献内容即可。

如果你的 RAG 系统允许用户添加内容 —— 服务台工单、Wiki 编辑、客户反馈、共享笔记 —— 那么你已经发布了攻击载体。问题在于你是否也同步发布了防御机制。