首页 核心备考 STAR 案例 评估与护栏 评估深入 护栏深入 核心概念 意图识别 EN

配套页 · 理由与取舍

STAR 形式的设计决策

来自 Mines Financial 项目的二十四个决策 — 当时的情境是什么、必须决定什么、我做了什么、最后出来了什么,以及每个选择付出了什么代价。按面试权重排布:评估与护栏占比最高。

如何使用本页

每张卡片是一个决策。其中的 接受的取舍 横幅,正是大多数候选人会跳过、而面试官真正在听的部分 — 一个说不出代价的决策,听起来就像你从来没有真正做过这个决策。

STAR 是什么 — 以及我在这里做了哪些调整

STAR 是一种面试回答结构: 情境(Situation), 任务(Task), 行动(Action), 结果(Result)。它之所以存在,是因为面对「讲一个你的技术决策」这种问题,人的本能是直接跳到技术上 — 「我用了一个 cross-encoder 重排器」 — 而这句话完全没有告诉面试官你是否真的理解了问题。

这四个部分强迫你补上缺失的上下文。Situation 是当时正在发生什么。Task 是具体必须决定或修复的是什么,它比情境更窄。Action 是 你 做了什么,用第一人称,包括你否决掉的那些选项。Result 是结果,最好带上一个数字。

本页做了两处调整。第一,我加了 接受的取舍 作为第五个部分,因为一个全是正面结论的 STAR 回答听上去像背稿 — 而对工程决策来说,代价本身就是推理的一半。第二,这里的 Result 常常是「这个行为变得可见/可度量了」,而不是「指标涨了」,对很多基础设施类工作来说,这才是诚实的结果。

不要把五个部分逐条念出来 口头表达时,这些内容压缩成大约四句话:一句讲情境,一到两句讲你做了什么、否决了什么,一句讲结果,一句讲代价。完整的卡片是他们追问时你才拿出来的储备。
备考时间该花在哪里 评估与护栏占本页 60%,因为职位描述在这两块写得最具体 — 「为概率型系统嵌入严谨的评估策略」以及「可解释、可审计、符合负责任 AI」。记忆和更大范围的架构也重要,但那是任何一个合格的开发者都能讲清楚的部分。前两块才是区分实践者和试验者的地方。

1 · 评估

30% · 最高优先级

六个决策,从「到底为什么要建评估框架」讲到一个改变了我对库默认值信任程度的实验。

1

为什么要建评估框架,而不是直接读答案

「你怎么知道你的改动是改进?」

情境

这个系统要回答五家公司、跨三个财年的问题,每个答案都堆满数字。我在改提示词(prompt)、检索参数和路由规则,而我唯一的反馈就是读几个答案、形成一个印象。这有两个问题。同一个问题两次不会给出同一个答案,所以单个好答案什么都证明不了。而当结果变差时,我无法判断是八九个步骤中的哪一个造成的。

任务

把「感觉变好了」变成可复现、便宜到能经常跑、并且具体到能指向某一个组件的东西。

行动

用行分隔的 JSON 文件建了四个测试集 — 路由、检索、计算和边界行为 — 总共大约 40 个用例,每个都有 id、查询、预期结果,以及一行 why_it_matters 说明它能拦住什么样的回归。

其中有两个决策值得一提。加载器会 拒绝 任何缺少必填字段的用例,包括 why_it_matters,这样测试集就不会悄悄堆满没人说得清价值的用例。另外,每个测试集只是注册表里的一条记录,把名字映射到一个用例加载器和一个评估函数,所以新增一个维度不需要动 runner。

结果

现在一个回归指向的是某个组件,而不是整个系统。失败会按用例打印出具体哪里对不上,只要有失败进程就以非零码退出 — 所以它已经长成了发布关卡的样子。

接受的取舍 四十个手写用例反映的是 我 想象中会出错的地方,而不是真实用户的行为。它能抓住粗粒度的回归,但支撑不了统计意义上的结论。而且「长得像关卡」不等于「是关卡」 — 目前还没有任何东西自动跑它。
2

两种运行模式:mock 与 real

「每跑一次都要花钱,你怎么做到经常跑评估?」

情境

跑一次真实模式需要 API key、建好的向量索引和关键词索引,而且每个用例都在烧额度。这就让它变成一件偶尔才做、还很不情愿做的事 — 而对一个本该尽早发现问题的检查来说,这恰恰是最要不得的性质。

任务

让一次零成本、零环境依赖的运行也能给出有意义的信息,同时不假装它证明了它证明不了的东西。

行动

加了一个 mock 模式,构造一份形状像成功运行的合成输出 — 预期的公司和年份、包含预期指标的合成文档、一个带引用的答案 — 然后把它送进 真实的 评估器代码。

我把这个局限写进了源码注释而不是 README:mock 模式不是用来证明检索器能用的,它证明的是评估逻辑和用例文件接对了。两种模式共用一条命令,通过一个 --mode 参数切换,另外还有 --suite, --limit 和 --id ,这样调试时真实运行可以缩到只跑一个用例。

结果

一个免费、确定性、离线的检查,能抓出格式错误的用例和坏掉的打分逻辑 — 这两种故障如果不管,会让每一次真实运行都不可信。真实模式则留给真正要看行为的时候。

mock 模式能证明什么

用例格式正确。评估器打分正确。某次重构没有搞坏框架。几秒钟跑完,零成本,每次结果都一样。

它证明不了什么

证明不了检索找到了正确的文档、路由分类正确,或者答案有据。一次全绿的 mock 运行对系统质量什么都说明不了。

接受的取舍 真正的风险在人:有人看到 mock 运行输出的「Overall: 40/40」,就以为系统没问题。缓解办法是在每次运行的头部打印当前模式,这比我希望的要弱。在企业规模下我会把两者改名 — 「框架自检」对「评估」 — 让它们在对话或看板里永远不会被混为一谈。
3

真实模式只跑仍然有意义的最省路径

「每个评估用例你都要跑整个系统吗?」

情境

一个路由用例只问一件事:给定这个问题,系统有没有正确地决定是去检索年报、搜网页、反问澄清,还是直接回答。为了知道这一点而跑完整个图,还要为最多五家公司的检索、每家一次重排调用、综合生成和两次评分调用买单 — 而这些都不是这个用例要测的。

任务

只测这个决策本身,不为决策下游的一切付费。

行动

真实模式下的路由用例直接调用路由节点和查询改写节点,手工拼出输出的 dict。检索用例调用的是检索子图,而不是整个应用。分发逻辑就是一个函数,按测试集选择执行路径。

一个细节:runner 还用正则从查询里推出几个字段 — 财年、指标关键词 — 因为图的状态并没有把它们暴露成顶层字段,而我宁愿在框架里确定性地推导,也不愿为了方便评估往生产状态里加字段。

结果

检测一个路由回归的成本从十来次模型调用降到两次。这就是「迭代时随手就跑的评估」和「发版前才跑一次的评估」之间的区别。

接受的取舍 它不是端到端的,所以抓不到交互型问题 — 比如路由单独看是对的,但它写进状态的东西被检索子图读错了。完整的端到端测试集仍然需要,只是不必对每个用例都跑。我想要的形状是:在一个覆盖面广的组件级测试集之上,叠一个小的端到端集合,而不是二选一。
4

一个把模型排除在回路之外的组件级 A/B 框架

「讲讲你是怎么衡量一次检索改动的。」

情境

我想知道,把候选池扩大并加上一个 cross-encoder 重排器,是否真的改善了检索。用最终答案质量去衡量这件事基本没戏:模型自身的方差比效应还大,所以答案的差异根本说明不了检索器的好坏。

任务

对比两套检索配置,让两次运行之间唯一不同的东西就是检索配置本身。

行动

写了一个框架,直接调用检索节点 — 不走路由、不做综合、不做评分,整个回路里没有任何模型调用。

  • 标准答案是一个精确的数字字符串 ,并且验证过它确实存在于入库的切块中 — 55,658 是 BHP FY2024 的营收, 29,016 是它的 underlying EBITDA。只要返回的文档里包含这个字符串,该用例就算命中,同时我记录它第一次出现的排名。
  • 指标: 最终截断内的命中数、前五名内的命中数,以及命中的平均排名。然后我按用例打印结论 — 变好、变差、 新命中,或 新丢失 — 因为总体指标可以变好,同时个别用例在回退,而事件(incident)正是由这些回退攒出来的。
  • 用替换构造基线: 把重排器换成一个直通函数来得到「旧」配置,而不是维护两条可能各自漂移的代码路径。
  • 两套配置之间必须清缓存, 因为候选数量是在关键词检索器加载索引文件时就固化进去的。漏掉这一步,第二次运行会悄悄复用第一套配置。
  • 重排器是一个有每分钟 token 限额的付费 API, 每个用例约 9,000 tokens,上限是 100,000,所以框架用一个固定的停顿给自己限速。没有它,对比会因为限流而失败,但看起来像是一个质量结论。
结果

一张按用例对比的表,其中每一处变动都能归因到配置本身。两套配置分别是:候选池 8、不做重排,对比候选池 20 + 20 重排后收到 8.

接受的取舍 三个真实的局限。精确字符串匹配严格且无可争辩,但它会漏算那些用不同写法表达同一数字的文档 — 这是指标的局限,不是检索器的。十二个用例、各跑一次,只能说明方向,说明不了显著性。而清缓存这个要求是颗地雷:任何扩展这个框架却忘了它的人,都会得到一个自信而错误的结论。
5

BM25 分词器实验:当教科书是对的,而库不是

「讲一次实验改变了你想法的经历。」

情境

我的关键词检索器只按空白字符切分文本,别的什么都不做。于是大写 R 的「Revenue」、小写的「revenue」和带逗号的「revenue,」是三个互不相干的索引键。这显然值得修。

有意思的是第二个问题:我要不要顺手去掉 停用词(stop words)?教科书的答案是不要: BM25 本身就已经在降低常见词的权重,所以停用词表是多余的。

任务

判断这个教科书结论在我实际用的那个库上是否成立,而不是在抽象的算法层面成立。

行动

我没有相信这个结论,而是去读了库的源码。对一个出现在大多数文档中的词,权重公式可能算出 负的 值 — 而这个实现拒绝负权重。它把每个负值都抬到一个下限,这个下限按平均权重的四分之一计算。

这就把原本的意图反了过来。接着我没有靠推理,而是去实测后果:

  • the 出现在我 84% 的切块中。它真实的权重是负的 — −1.626 — 而那个下限把它抬到了 1.811。这比 Group 的 1.542还高,而 Group 是一个真正有信息量的词。
  • 只有 8 个词受影响。但这八个词几乎出现在每一个自然语言问题里。
  • 对查询「what was the revenue of the group in 2024」, 59% 的排名第一文档得分来自 what, was, the, of, in.
  • 去掉它们之后,新的前五名文档与旧的前五名 零 重叠。

然后是归一化,用同样的方法测量:匹配 revenue 的文档从 221 增加到 278,提升 26%;词表从 37,933 缩到 23,676,下降 37.6%。词表变小的价值不在于省内存 — 而在于文档计数变正确了,于是被虚高的权重回落到了真实水平。

结果

五个里零个重叠,这不叫排序被扰动了。它意味着结果本来就是由填充词决定的,而这件事我靠看答案永远发现不了 — 只有读实现、打印每个词项的得分贡献才能发现。

我自己的修复引入的 bug — 以及我为什么庆幸它发生了

一个完全由常见词组成的问题 — 「what is it」 — 分词后变成了空列表。我追查了空查询在检索器内部会发生什么:每个文档得分都恰好是 0.0,top-n 选择按分数排序,于是返回二十个任意的文档。没有异常、没有告警,没有任何信号表明这个查询已经被毁掉了。

这比崩溃更糟。崩溃会在出现的当天被修掉;而这个问题会一直待在那里,对一整类问题返回看上去很像样的垃圾。

修复只有一句话 — 当过滤会把列表清空时,就返回未过滤的 token — 而我把它放在了分词器 内部 ,而不是放在各个调用点,因为建索引和查询共用这一个函数,改一处就同时覆盖两条路径。模块的自检里有一条断言守着它。

可以推广的教训 停用词这个改动既是一次改进,又引入了一种静默故障。两件事同时为真,如果我只测了排序的提升,就会在毫无察觉的情况下把第二件事一起发出去。先度量改动本身;然后再去找这个改动让什么变成了可能。
接受的取舍 停用词表是 35 个固定的英文词,是手工挑的,而不是从语料频率里推导出来的 — 所以它没法迁移到别的领域或语言。财务文本还逼出了一些特殊处理:标点只从 token 的两端剥掉,这样 $1.2B 和 16% 才能保留下来;带千位分隔符的数字会被索引两次,一次是 53,658 ,一次是 53658。这是领域专属的工作,通用分词器不会做,换一个领域就得重做一遍。
6

记录那些我发现了、却刻意不修的问题

「还没来得及处理的技术债你怎么办?」

情境

在交付分词器改动的过程中,我发现了两件与它无关的事。那个工作副本里没法重建关键词索引 — 既没有向量数据库,也没有 API key — 另外,有两个模块把向量库指向 chroma_db/ ,而实际存在的数据库位于 data/chroma/。两套路径约定,互相打架。

任务

决定在做无关工作时发现的一个真实问题该怎么处理。

行动

两个诱人的选项我都没选。不在一个无关的改动里顺手修掉,那会让这次改动没法评审。也不闭口不提,因为已知问题正是这样变成未知问题的。

我把两件事都记进了项目的 debug 日志,标上状态 — 未解决 或 已知局限 — 附上定位到它的证据,以及还剩下什么要做。我给自己定的项目规则是:记录里必须写上实测的数字和我是怎么发现的,而不只是结论。「把 K 改成 20」毫无价值;「 the 的权重被抬到了 1.811,高于 Group 的 1.542」才是真正的发现。

结果

路径不一致的问题还在,但它连同证据一起被写了下来,而不是只存在我脑子里。等谁撞上它的时候,诊断已经做完了。

为什么这件事该出现在一场关于评估的面试里 这个岗位有一部分职责是塑造「NAB AI 团队的技术文化」。文化类问题不是靠描述一个工具来回答的 — 而是靠展示一个没人监督时你依然保持的习惯。「我会把决定不修的问题连同证据一起记下来」是一个小的、可验证的、不光鲜的习惯,而恰恰是这种习惯才可信。
接受的取舍 写下来的问题仍然是问题。台账只有被人读才有用,而我的台账没有负责人、没有优先级,也没有复盘节奏。到了团队规模,这应该是有负责人的工单,而不是一个 markdown 文件 — 纪律是对的,机制撑不过一个人。

2 · 护栏

30% · 最高优先级

七个决策。注意其中有多少是关于护栏自身的故障模式 — 误报、静默错误、告警文案本身 — 而不是关于它执行的那个检查。

1

最后一道关卡放在边界上,而不是每个分支里

「你把检查放在哪里,为什么放那里?」

情境

有两条相当不同的路径会走到生成答案这一步。一条是检索路径,它收集文档、起草、打分。另一条是直答路径,路由判断不需要检索,直接就地回答 — 比如打招呼,或者「EBITDA 是什么的缩写」。第二条路径完全不碰检索,所以检索侧的检查对它一个都不生效。

任务

保证不会有空的或退化的内容送到用户面前,任何路径都算,包括还不存在的路径。

行动

在所有进入答案节点的边上放同一道关卡,而不是在每个分支里各写一个检查。它是确定性的,不发任何网络请求,只有两条规则:聚合后的上下文缺失或为空就判失败,短于最小长度也判失败。

失败会路由到一个兜底节点,它不只是道歉 — 它会区分「什么都没检索到」和「检索到了文档但无法综合」,因为这是两种不同的失败,然后列出实际覆盖的公司、财年和指标,并给出两个示例问题。

结果

任何新增的、通向答案节点的分支都会自动继承这个保证 — 这是结构上的性质,而不是后来的贡献者必须记住的事。五个单元测试把行为钉死,包括纯空白输入和去空白的顺序。

接受的取舍 它就是一个十个字符的长度检查。我宁愿直说,也不想让面试官自己发现:它是防空输出和退化输出的下限,不是内容过滤器。真正的校验在上一层的有据性(groundedness)检查里。这个下限之所以值得存在,恰恰是因为它笨 — 没有模型、没有网络,没有任何可能出错的东西。
2

有据性:一次调用里做两个判断,用两种相反的严格度

「你怎么发现一个被幻觉出来的数字?」

情境

在财务问答里,真正要命的失败是一个说得很笃定的错误数字。相比裸模型,检索系统在这里有一个优势:我确切知道草稿是基于哪些文档写出来的,所以有东西可以拿来核对。

任务

检出没有被检索证据支撑的主张,同时另外检出没有有效内容的答案 — 并且不让一个判断污染另一个。

行动

一次评分调用,输入问题、已打分的文档和草稿,做两个明确分开的任务,返回一个严格的结构 — 一个通过/不通过的判定,加一份无支撑主张的清单。

两者的严格度是刻意相反的。通过/不通过这个任务被要求 宽松 — 只有当答案为空、或者通篇只有免责声明时才判失败 — 因为这道关卡的存在是为了抓「我没有足够的信息」,而不是管质量。有据性任务则被要求 严格:只有当一个主张的数值在所有文档中都确实找不到时,才标记它。

然后这个数量映射到一个判定:零个无支撑主张是 yes,最多三个是 partial,再多就是 no — 而这个判定就成为展示给用户的置信度:高、中或低。

结果

展示给用户的置信度来自一次真正跑过的检查,而不是问模型「你有多确定」 — 后者给出的是一个背后什么都没有的数字。中、低置信度会附上一段提示,点名具体是哪些主张;高置信度什么都不附,因为每个答案都带提示就成了噪音。

接受的取舍 两个判断放在一次调用里更便宜,但也把它们耦合在了一起 — 一次糟糕的抽取会影响判定。按主张的原始数量设阈值很粗糙:一个横跨五家公司的长对比里有三个无支撑主张,和一个只给一个数字的答案里有三个,完全是两回事。而且「有据」不等于「正确」 — 一个主张可以和文档完全对得上,但如果这份文档在入库时就被读错了,它依然是错的。这个检查约束的是编造,不是真相。
3

表格单位:别让护栏喊「狼来了」

「你的检查在正确答案上误报时发生了什么?」

情境

财务表格把单位写在表头单元格里,数据单元格则是光秃秃的数字。于是一个切块里有一行表头写着 US$M ,一行数据写着 29,016。答案正确地写成「US$29,016 million」。校验器看到的是两个不同的字符串,于是把一个正确的主张标成了无支撑。

这不是偶发。凡是从表格推出来的答案,它都会系统性地出现。

任务

让校验器能够把一个裸数字和它的单位对应起来 — 同时不要放松到连真正的编造都抓不住。

行动

分两层。第一层是结构性的:对来自表格的文档,扫描表头行里的单位 token — US$M, A$B, A$'000, % 等等 — 然后在内容上方注入一段显式说明,写明本表中所有裸数字都以该单位计。这个「重新接上」发生在模型看到文档之前,而不是留给模型去推断。

第二层是指令性的:在提示词里明确写出哪些差异不算无支撑。单位和量级的写法、小幅四舍五入、表示负数的会计括号、指标名的变体(比如用 underlying EBITDA 支撑一个关于 EBITDA 的主张),以及「year ended 30 June 2024」就是 FY2024。

结果

正确答案不再被标记了。这件事比听上去更重要:一个在正确答案上频繁触发的告警,会训练用户忽略所有告警,从而毁掉那个真正重要的告警的价值。

接受的取舍 单位识别是一条针对固定 token 集合的正则。如果一张表把单位写在周围的正文里或脚注里,仍然会被漏掉。而且我每加一条为减少误报而设的容忍规则,就等于收窄了这个检查能抓到的范围 — 这个平衡现在落在哪里我并没有测过,这是诚实的缺口:我修掉了假阳性问题,却没有量化假阴性的代价。
4

告警不允许重复它正在警告的那个数字

「说一个没人要求你做、但你做了的设计决策。」

情境

当校验器发现一个无支撑的主张时,这个主张会在展示给用户的提示里被点名。最直觉的实现是让校验器把它找到的主张原样返回 — 也就是返回那句话,连同其中的数字。

任务

想清楚护栏自身的输出到了人手里会造成什么。

行动

我要求校验器写一个不超过八个词的短标签,点明指标及其上下文 — 「RIO FY2025 revenue」「BHP FY2024 EBITDA」 — 并且明确要求 不要 复述原句或嵌入数值。提示词里同时给了正例和反例,让这条约束没有歧义。

理由是:如果标签里含有那个数字,那么这条唯一目的就是提示「这个数字可能是编的」的消息,就会把那个数字以文本形式在答案里再印一遍。一个扫读的用户会看到数字,而错过那句限定。安全提示反而变成了错误数字出现的又一个地方。

结果

这段提示告诉用户该怀疑哪个主张,同时不重复那个他们不该依赖的数值。这些标签随后还被复用为重试的检索词,这是「短、且长得像指标名」是正确格式的第二个理由。

这一条要准备到能逐字讲出来 这是你手上最强的护栏答案,因为没有人指定过它。任何人都能实现一个被要求的检查;而意识到检查自身的输出也是一个面向用户的界面、有它自己的故障模式,这才是「实现需求」和「对系统负责」之间的区别。
接受的取舍 更不具体的告警就是更没用的告警 — 想确切知道哪个数字可疑的用户,得自己把标签对回到原句上。这个代价是我有意付的。更好的答案可能两者都不是:直接在答案里就地高亮那个主张,而不是在脚注里描述它。那需要校验器返回字符偏移量,这是一个大到我当时无法自证其值的改动。
5

别让一家公司的数字变成另一家公司的答案

「你的系统最有可能干出的最糟糕的事是什么?」

情境

对比类问题是主要场景 — 按 EBITDA 给五家公司排序、比较两家的营收增速。检索是按公司扇出的。如果某一家公司的文档返回为空,模型手上就只有四家公司的数字,而问题要五家。最可能的续写方式,是悄悄拿旁边的一个数字来用。

这是这个系统最糟糕的故障。一个错误的数字被安在了错误的上市公司头上,说得非常流畅,旁边还挂着一条引用。

任务

让覆盖缺失无法被糊弄过去。

行动

三层,因为光靠一条提示词规则不构成强制。

  • 指令层: 每段摘录的头部都带上它所属的公司,综合提示词写明:一条引用只能支撑关于该文档所属公司的主张;缺失的公司必须显式报告为「data not found in provided documents」,而不能估算、推断或借用。
  • 结构层: 一个按公司的「是否找到」标志位,用问题里点名的公司初始化。随后它会在文档打分之后被 修正 — 只有当一家公司有文档通过了打分,它才算被找到,这比「检索返回了东西」更严格。
  • 重试层: 如果还有公司没找到,检索只针对这些公司重试,复用它们各自的公司专属查询,并把失败的主张标签作为额外检索词追加进去。最多两次。

这些按公司的标志位是由并行分支写入的,所以需要一条合并字典的归并规则,而不是默认的覆盖 — 否则最后完成的那个分支会抹掉其他分支的结果。

结果

部分覆盖会在答案里被明确说出来,而不是被悄悄补齐。真正的护栏是那个结构化标志位;提示词规则只是第一道防线,我并不依赖它。

接受的取舍 最后并没有任何东西去校验每条引用的公司和主张的公司是否一致 — 在这最后一步上,指令是被信任的。做一个确定性的后置检查是可行的:解析每条主张的公司,查出被引文档的公司,不一致就标出来。这是我接下来会做的;它比任何提示词措辞都更强,而且并不难。
6

引用完整性:不允许出现指向空处的标号

「如果只有五个来源,模型却引用了第七个,怎么办?」

情境

模型拿到的是一份编号的来源列表,并被要求在每个事实后面放上类似 [1] 或 [1,3] 这样的标号。有两件事会自己出问题。它有时会引用列表之外的编号。而且它通常只用到给它的八个来源中的三个,留下五条参考条目在答案里什么都不支撑。

任务

保证展示给用户的每一个标号都能解析到一个真实来源,并且参考列表里只包含答案实际用到的东西。

行动

一个固定的后处理顺序,而这个顺序就是整个设计。

  • 校验: 扫描答案里的标号,只保留落在有效范围内的编号,去重,并保持首次出现的顺序。编造出来的编号被丢弃,不会渲染出来。
  • 重新编号: 把留下来的编号重映射成从 1 开始的连续序列,这样读者不会看到从 [1] 直接跳到 [4]。
  • 过滤: 只用被引用到的来源构建参考列表,并按公司、财年和页码去重,这样同一页的两个切块不会变成两条条目。
  • 最后 把标号转换成带索引属性的上标 HTML,前端用这个属性做悬停预览。

最后这一步必须放在最后:先做上标会破坏前几步匹配用的模式。这一点写进了函数的文档字符串,因为从调用点看不出这个顺序约束。

结果

没有悬空的引用,也没有用不上的参考条目,所以引用列表是答案实际用了什么的精确记录 — 正是这一点让一条主张可以拿回原始 PDF 去核对。

接受的取舍 这只校验了一个标号能 解析到东西,而没有校验它 正确。一条指向真实但错误页码的引用,能通过上面每一项检查。这是有据性问题,不是格式问题,而且它没有被完全覆盖:有据性检查确认的是某个数字存在于检索结果集中的某处,而不是被引用的那份具体文档就是包含它的那一份。
7

fail-open 矩阵:逐层决定失败的方向

「你的护栏抛了异常,然后呢?」

情境

流水线里几乎每一个检查都是对第三方的网络调用 — 两个打分模型、一个重排 API、一个路由模型。每一个都可能超时、被限流,或者返回无法解析的东西。放着不管,每个异常处理分支最后都会在无意中替你做了一个可用性决策。

任务

逐层决定:这个检查失败时,是应该放行请求还是拦住它 — 而且要有明确写下来的理由,而不是凭习惯。

行动

我写下了一条规则并一致地执行。如果这一层属于 精确率提升 — 它让一个好答案变得更好 — 那就 fail open,因为失去它只是让质量下降,而卡在它上面则是让服务消失。如果这一层属于 正确性底线 — 没有它,一个错误或空的答案就会到达用户 — 那就 fail closed。

  • 文档相关性评分器 → 失败放行(fail-open)。 出错时,把检索到的文档全部放过去,而不是一份都不放。上下文吵一点,也好过没有上下文。
  • 有据性(groundedness)评分器 → 失败放行。 出错时判为通过。这是我最不放心的一条,我也会直说:它本身就是幻觉检查,而它的失效是无声的。
  • 重排(rerank)→ 失败放行。 出错时保留融合后的排序。它只是在一份已经存在的排名上再做一次精确率(precision)打磨。
  • 路由器 → 走「安全」,而不是放行或拦截。 出错时默认去检索,绝不默认卡在澄清上。多检索一次只是花钱;拒答一个合理的问题,损失的是信任。
  • 最终内容闸门 → 从结构上就是失败拦截(fail-closed)。 确定性的,不发网络请求,没有可失效的东西。这正是为什么底线那一层要做成最笨的一层。
结果

没有任何单个第三方故障能把答案链路整条打掉,而且每一次降级都是主动选择的。它同时还产出一份可以直接交给风控的干净材料:一张列出层级、失效模式、失败方向和影响范围(blast radius)的表。

已接受的取舍 上面每一处失败放行,都在无声地吞掉异常。没有计数器,也没有告警,所以系统可以在有据性检查实际上已经关闭的情况下照常运行,看上去还非常健康。真正的缺陷是「无声地失败放行」,而不是失败放行本身 —— 到了生产环境,给它们加上埋点是我要做的第一件事。

3 · 记忆

20%

五个决策。最后一个在面试现场最有用,因为正是在那里,记忆、评估和护栏被发现其实是同一套机制。

1

四层记忆,以及它们为什么按这个顺序跑

「在你的系统里,记忆到底指什么?」

情境

「加上记忆」这句话,其实是四个互不相干的需求:对话要能扛过重启;历史不能涨过模型的上下文上限;追问要保住它的主语;重复的问题不该按全价再算一遍。

任务

把每一条都做成独立的一层,各有各的存储和生命周期,并决定它们的执行顺序。

行动
  • 持久化状态: 整个图状态在每一步之后都会按线程(thread)为键检查点写入 PostgreSQL。既能扛住重启,也让一次运行可以回放。
  • 压缩: 消息超过 10 条之后,除最近的 4 条以外,其余全部压缩成一条摘要,原文删除。如果摘要生成失败,什么都不删 —— 一次失败的压缩绝不能弄丢历史。
  • 实体延续: 从最近的 6 条消息里抽出公司代码和财年,用来补全缺了这些信息的问题。
  • 答案缓存: 历史问答对放在自己独立的向量集合里,和报告切块(chunk)分开。相似度高于 0.95 时直接返回已存答案、跳过检索;介于 0.80 和 0.95 之间时照常跑完整条流水线,但把旧答案作为引子喂进去。

这个顺序是承重的:先压缩,实体扫描读到的才是干净的历史;再补全,缓存查找用到的才是补全后的问题;缓存放最后,一旦命中就能跳过下游的全部工作。两个彼此分开的向量集合意味着,系统永远不可能把自己过去的答案当成年报来引用。

结果

四套机制,每一套都能单独推理、单独测试、单独关掉 —— 而不是一个谁也说不清它会怎么表现的「记忆」组件。

已接受的取舍 四层意味着改动其中任何一层之前都要先理解四件事,而那条顺序依赖在代码里是看不见的 —— 它只活在一个 docstring 里。压缩的触发条件是消息条数而不是 token 数,这在每轮长度都差不多时没问题,一旦不均匀就是错的。
2

两级缓存,而不是一个阈值

「你怎么判断一个缓存答案够不够接近?」

情境

单一相似度阈值逼你做一个难受的选择。定低了,复用变多,同时也换来一堆自信的错答案 ——「BHP revenue FY2024」和「BHP revenue FY2023」作为句子几乎一模一样,作为问题却完全不同。定高了,缓存几乎不会命中,那还不如不做。

任务

既要从近似匹配里榨出价值,又不能把答案押在一个相似度分数上。

行动

把这个决策拆成两半。高于 0.95,当作同一个问题,直接返回已存答案。介于 0.80 和 0.95 之间,就不把它当答案信 —— 照常跑完整条流水线,只是把旧答案带进合成环节,标注为「针对相似问题的既往分析,仅供参考」,并明确要求无论如何都要把答案落在检索到的原文片段上。

中间那一档改变的是失效模式。在那一档里出现一个错误的近似匹配,不会直接产出错误答案;最坏也就是一段有误导性的上下文,而检索到的证据会盖过它。

结果

相似度分数最不可靠的那一段区间,恰好就是犯错代价最低的区间。缓存答案还会带上自己的来源列表,所以缓存命中给用户看到的引用和实时回答一样,不会显得像一句没有出处的断言。

已接受的取舍 这两个数字都是凭判断定的,没有在一份标注好的改述句对数据集上拟合过,我也没有测过误命中率 —— 所以我目前答不出「缓存多久命中一次,其中多少次是错的」。更糟的是,换一个向量(embedding)模型会悄无声息地把所有分数挪位,让两个阈值同时失效,而没有任何测试能拦住。
3

缓存里存的是问题的向量,答案挂在元数据上

「你缓存里的那个向量,到底是什么?」

情境

直觉上会想去存答案,因为答案才是值钱的那部分。可是查找的时候,我手上只有一个问题。

任务

选对被存下来的那段文本,让真正发生的那次比较,就是我想要的那次比较。

行动

把问题存成可检索的内容,答案、公司、财年和来源列表作为元数据挂在旁边。于是检索比的是问题对问题。

拿问题去比答案,是两种文本形态的错配 —— 问题短而带疑问,答案长、满是数字和限定条件。即便它们本就是一对,在向量空间里也落在不同区域,于是分数变得嘈杂,阈值也就失去意义。

存下来的是 补全后的 那一版问题,所以一个被解析成「dividends [BHP, FY2024]」的追问,日后能被直接问出完整问题的人检索到。

结果

相似度分数变得有意义,而这正是让阈值成为一个决策、而不是一次瞎猜的前提。

已接受的取舍 问题对问题的匹配会漏掉一种情况:一个措辞不同的问题,本可以由同一条已存答案来回答。把问题和答案都做向量、再融合结果可以覆盖它,代价是双倍存储加一条要调的融合规则 —— 在当前命中率还没被测过、也没被证明不够用之前,不值得做。
4

用正则做实体延续,而不是让模型改写问题

「为什么不干脆让模型把追问改写一遍?」

情境

「那他们的分红呢?」这句话里既没有公司也没有年份。原样送进检索,它会在五家公司里搜「分红」这个词,然后返回一堆噪声。

任务

在检索之前把指代解析掉,并且用能覆盖实际发生的那些情况的、最便宜的机制。

行动

选了窄而确定的那条路,而不是模型改写。在这个领域里,歧义几乎永远落在同样的两个槽位上 —— 哪家公司、哪个财年 —— 所以我用两条正则加一张小的全称别名表去扫最近的消息,只补当前问题缺的那部分。

有三个细节让它是安全的。它是倒着往回扫的,所以用户换公司时,最近提到的那家会赢。它只填真正为空的槽位;如果问题里两样都已经点名了,它压根不往状态里写。还有,缓存查找用的就是补全后的问题,于是查找和保存共用同一个键 —— 否则你按一段文本保存、按另一段文本检索,永远不会命中。

结果

零额外延迟、零额外成本,完全确定、可做单元测试,而且它不可能凭空编出一家从没被提过的公司 —— 而模型改写偶尔就会。

模型改写

能处理任何类型的指代,包括「把那个和另外那个比一下」。代价是每一轮都要多一次调用和一段延迟,结果不确定,而且可能往查询里幻觉出一个实体。

正则实体延续

免费、瞬时、确定、可测。但只覆盖列出的那几类实体 —— 像「那毛利率呢?」这种指向指标的指代,它完全处理不了。

已接受的取舍 它是刻意做窄的,也不会泛化:五个硬编码的股票代码、一种年份格式、四个别名。任何指向某个指标、或指向上一条答案内容的表达,都在覆盖范围之外。诚实的升级路径是上模型改写,同时把这套规则留作确定性兜底,用于改写失败或改写乱编的时候 —— 是互补,不是替代。
5

由评估器决定什么东西配被记住

「什么东西能拦住一个坏答案被缓存下来、然后一直被端出去?」

情境

缓存系统自己产出的答案,有一种文档缓存不会有的失效模式:它会自我投毒。一条写进缓存的错答案,会变成一条永久的错答案,被自信地端给每一个提出相似问题的人 —— 而且比正确答案来得 更快 ,因为缓存命中会把检索整个跳过。

更糟的是,它是隐形的。没有报错,没有失败的检查,日志里什么都没有。系统看上去只是运转得格外顺利。

任务

决定一个答案凭什么资格能变成记忆 —— 而且这个资格不能只是「我们产出了它」。

行动

把写入这件事挂到已经存在的护栏层上。只有当质量评分器判它通过, 并且 有据性判定不是「否」时,答案才会被保存。低置信度的答案照样会送到用户面前 —— 带着它的警示语 —— 但绝不写进记忆。

第二个决策是顺序,而这才是值得主动拿出来讲的那部分。保存发生在追加置信度警示语 之前 ,所以被缓存下来的是干净的答案。否则,将来问相似问题的用户会收到一条陈旧的警告,讲的是一次和他的问题毫无关系的验证运行 —— 警示语活得比产生它的那份疑虑还久。

总结成一条原则:读路径乐观,写路径悲观。缓存未命中的代价是几秒钟和一点额度。把缓存投毒的代价,是关于一家上市公司财务状况的错答案,反复被端出去,直到有人发现。

结果

记忆的质量被同一个检查所约束 —— 那个检查同时也在决定告诉用户该信多少,于是两者不可能各自漂移。这也是我三个主题唯一汇合成同一套机制的地方:评估器 就是 运行时护栏,而护栏又是记忆的闸门。

如果他们让你把三个主题串起来,就这么答 「在我的代码里,它们只在一个地方碰头 —— 缓存写入由有据性评分器把关。那个评分器是我的评估组件,它同时充当答案的运行时护栏,并且决定什么被允许成为记忆。一个检查,三种角色。」面试官之所以要问关联,正是因为只读过这些主题的候选人,会把它们装在互不相通的盒子里。
已接受的取舍 拿评分器把关,就继承了评分器自身的错误率,而这个错误率我从没测过 —— 一次误判「通过」就会把一条坏答案写进记忆,而下游没有任何环节会再复查它。读路径上根本没有任何校验:缓存命中会绕过所有护栏,因为那个答案只在写入时被检查过一次。而且条目永不过期,所以一条针对某一次入库运行做过有据性校验的答案,在那批文档被替换之后依然在线。这两点都需要我会最先去做的那套版本戳和过期机制。

4 · 检索、入库与编排

20%

六个决策,覆盖前三节所立足的那部分架构。篇幅更短,结构相同。

1

三段式检索:关键词、向量,然后是一个真正读内容的模型

「为什么不直接用向量检索就好?」

情境

在这份数据上,两种单一方法都会失手。只用向量检索,在精确 token 上很弱 —— 股票代码、「FY2024」、某个具体数字 —— 因为基于语义的匹配并不会偏袒一个精确字符串。只用关键词检索,则完全漏掉改述:「盈利能力」匹配不上「EBITDA」。

把两者结合起来,又暴露出第三个问题。融合两份排序列表靠的是排名 位置 ,从头到尾不读正文,于是一个含有确切数字的切块,可能排在两种方法都喜欢的、主题相近的套话下面。

任务

两种召回率(recall)都要拿到,然后交给一个真正读得懂内容的东西去决定谁留下。

行动

三个阶段。关键词检索和向量检索各返回 20 个候选,按权重融合:关键词 0.45 ,向量 0.55 —— 向量权重略高,因为改述是更常见的需求;代码里的注释也写明,如果精确的金融术语开始变得更重要,就把关键词权重调上去。

然后由一个交叉编码器(cross-encoder)拿问题逐一比对这四十个候选,返回最好的 8条。把候选池放宽抬高的是「能被找到」的上限;真正读内容的那一层决定截在哪里。下游 token 成本不变,因为最终条数是一样的。

有一条实现细节值得随身备着:重排客户端是每次调用现构的,因为客户端持有一个非线程安全的 HTTP 会话,而五条公司分支是并行跑的。构造耗时是微秒级,对上一次约 800 毫秒的 API 调用,可以忽略。

结果

在不抬高上下文成本的前提下抬高了召回上限,最终选择由内容而不是排名算术决定。这正是评估那一节第 4 张卡片里,由 A/B 测试台架测出来的那次改动。

已接受的取舍 每条公司分支多一次付费 API 调用和大约 800ms 延迟,而且在答案链路上多了一个第三方依赖 —— 靠失败放行回落到融合排序来缓解。融合权重是手工定的、从没调过;那是我在宣称它们正确之前,会先扔进一轮评估扫描的两个数字。
2

每家公司一份关键词索引,再做并行扇出

「一个问题同时问五家公司,你怎么处理?」

情境

如果只有一份全局关键词索引,一个关于 FMG 的问题,会把二十个候选名额大半花在恰好分数高的那几家公司身上 —— 而 BHP 的报告长得多,于是它们会占据主导。做五家公司的对比时,披露最薄的那家会被整个挤掉,而这恰恰就是第 2 节第 5 张卡片里说的那种护栏失效。

任务

保证每一家被问到的公司都拿到属于自己的召回预算。

行动

每家公司一份关键词索引文件,只用这家公司的切块来构建;再加一层向量库元数据过滤,把语义检索限定在同一家公司。一个查询改写步骤会把问题拆成每家公司一条的专属查询,图再按公司扇出成一条条检索分支并行执行。

合并并行分支需要明确的规则:检索到的文档按列表拼接累积,每家公司的「是否找到」标志用字典合并规则,这样分支之间不会互相覆盖键。重试时,扇出天然只会打向仍然缺失的那几家公司,因为改写步骤已经把查询列表收窄了。

结果

每一家被点名的公司都拿到自己的二十个候选和自己的一轮重排,所以一次五家公司的对比有五份互相独立的召回预算,而不是一份共享的。整体墙钟时间接近单家公司的延迟。

已接受的取舍 要重建并保持同步的索引文件从一份变成五份;而一个没点名任何公司的泛问题,会回落成五家全搜 —— 对一个可能并不需要的问题付出五倍成本。扇出还会把重试成本乘上去:五家公司、两次尝试,就是十轮检索外加重排,这也是为什么重试上限既是一个正确性决策,同样也是一个预算决策。
3

先把页面筛出来,再为视觉模型付费

「年报里的表格才是重点。你怎么把它们取出来?」

情境

从 PDF 里做文本抽取会毁掉表格。一张财务报表会变成一串数字,列表头、单位和行标签全都断开 —— 而这恰恰就是这个产品存在的意义所要回答的那部分内容。

视觉模型能把表格当成表格来读。但这些是横跨十五个报告年度的三百页文档,其中大部分页面是叙述性文字,视觉模型在上面毫无增益。

任务

找出值得为之调用视觉模型的那些页面,然后从中拿到结构 —— 而不只是文本。

行动

两个阶段,在昂贵的模型前面放一个便宜的确定性筛选器。

  • 筛选: 用一个 PDF 库检测表格,再把它和 数字密度 结合起来 —— 数字字符除以非空白字符 —— 要求高于 5% 的数字占比、至少 20 个数字字符,以及至少 100 个文本字符。矢量绘图路径的数量用来抓图表。这里还有一条专门针对常见误判的保护:一块被检测为「表格」、却覆盖了页面 80% 以上的区域,通常其实是双栏文本排版,所以这类区域改按绝对计数而不是密度来判断。
  • 抽取: 一个视觉模型,配一份显式的嵌套单元格 JSON schema —— 表头、行、跨列,以及会再拆成子行的单元格,深度设有固定上限 —— 外加四张带人工手写正确答案的少样本页面图。它被要求:任何读不出来的单元格一律输出 null,而不是猜。

运维层面:并行 worker 加检查点文件,让一次运行是续跑而不是重跑;向量化按批处理并配速率控制,以守住 API 的每分钟 token 上限。

结果

表格能以结构的形式活着通过入库,这也正是后面有据性检查能把一个光秃秃的「29,016」重新接回它的「US$M」表头的原因。提示词里还带着一些踩坑换来的具体规定 —— 比如「=」号永远不是单元格分隔符,因为高管薪酬表老是被从某个门槛条件的中间劈成两半。

已接受的取舍 这个筛选器是启发式的,所以一张稀疏但重要的表格 —— 一页基本是叙述、只有五个数字 —— 可能被丢掉,而下游整条流水线永远不知道它存在过。我从没测过这个筛选器的召回率,这才是这里真正的缺口:正确做法是把一整份完整报告里的表格人工标注出来,再拿筛选器去对着打分。这个项目里其他所有环节都有一个数字,唯独这一段没有。
4

算术是一次工具调用,不是让模型在脑子里算

「问题需要算增长率的时候会发生什么?」

情境

有相当一部分问题需要在检索到的数字上做算术 —— 增长率、利润率、三年平均。语言模型会算错,而一个带着自信错误增长率的金融答案,比不给答案更糟,因为它看起来和正确答案一样权威。

任务

让算术变得确定且可检查,并且决定什么时候才需要它。

行动

用一个带类型化工具的智能体,而不是自由式的心算:从一组带匹配期间标签的时间序列算增长率和复合年增长率、算比率、算平均,再加一个通用表达式求值器兜住不适配的那些步骤。

这里面有两个决策。通用求值器用的是一个受限的表达式库,而不是语言内置的求值函数,这样模型生成的字符串不可能执行代码 —— 如果你顺手去拿那个方便的函数,那就是一个明摆着的注入面。另外,这些工具会校验输入并返回错误字符串,所以数组长度不匹配或除零,回来的是一条智能体可以据此反应的消息,而不是一个把整次运行打断的异常。

至于 是否 需要算术,这个判断是在检索和聚合之后做的,不是从问题本身做的 —— 所以它是对着实际拿得到的数字来判断,而不是从措辞上猜。

结果

每一个算出来的数字都来自真正的算术,而且工具输出会把过程摊开 —— 这让一个推导出来的数字,可以像一个被检索到的数字那样被审计。

已接受的取舍 凡是涉及算术的问题都要多一轮智能体交互,所以这一类问题的延迟和成本都会上升。受限求值器刻意做得比通用求值器弱,它会拒掉一些用户合理想写的表达式。而且这些工具保证的是算术本身没错,不保证进去的是对的数字 —— 挑错了输入数字仍然是一种可能的失效,而那属于有据性问题,不是计算问题。
5

在一个拒绝标准做法的服务商上拿到结构化输出

「你怎么从模型里稳定拿到结构化的决策?」

情境

这张图是按模型的决策来路由的 —— 要不要检索、要不要新闻、要不要澄清、是否超出范围。这些必须以可靠的类型化字段回来;一个需要被解析的自由文本答案,迟早会被解析错,然后把请求送进错误的分支。

框架默认的那套 schema 强制方式,被我用的这家模型服务商拒绝。

任务

在这家服务商上拿到 schema 强制的输出,并且让这个约束对下一个读代码的人是可见的。

行动

把每个决策声明成带默认值和逐字段描述的类型化 schema —— 这些描述本身就是模型看到的提示词的一部分,是在干实活的 —— 并把结构化输出方式强制改成函数调用,而不是走默认。温度全线设为零,因为这些调用没有一个想要创造力。

存在两个独立的模型客户端,是有具体理由的:一个小的,输出上限 1,000 token,用于路由和评分;一个大的,上限 5,000,用于合成和最终答案。一次路由决策,不该被误按合成调用来计费和分配延迟预算。

服务商的这条约束,被写成一行注释放在每一处出现的上方,因为它从代码本身看不出来 —— 某个来「顺手整理一下」那个方法参数的人,会把路由弄坏,而且只有到运行时才会暴露。

结果

路由决策以类型化布尔值的形式回来。这些调用每一个也都被包了一层,让失败回落到一个安全默认值,而不是往上抛。

已接受的取舍 这是一个绑定服务商的选择,而且在每个调用点上重复了一遍,所以换服务商意味着要把它们全找出来重测 —— 更整洁的设计是用一个包装层来独占这个决策,等到真有第二家服务商进场时我就会那么做。函数调用作为保证,也比 schema 约束的响应略弱一些;而两个模型客户端意味着改一个模型名要改两个地方。
6

一次可持久化的暂停,让「提问」不是一条死路

「从请求澄清到拿到澄清,这中间发生了什么?」

情境

当一个问题太含糊时,正确的行为是去问。但去问意味着在图执行到一半时停下来等人,而这个等待可能是一分钟,也可能是一周,中间还可能跨越一次服务重启或一次部署。把它揣在内存里,等回复到了,接住它的就是一片虚无。

任务

把执行持久地暂停住,再带着用户的回答从同一个点恢复。

行动

在 PostgreSQL 检查点之上用了框架的 interrupt 机制:状态本来每一步之后就已经持久化了,所以所谓暂停,不过是「没有下一步」而已。API 通过对同一个线程发送 resume 命令来恢复,澄清节点则带着补全后的问题,直接绕回路由决策。

连接池需要一些不那么显然的设置 —— 打开 autocommit,并把预处理语句阈值设为零,这才是它能在连接池代理后面正常工作的关键。执行进度以服务器推送事件(SSE)流式传给前端,用户看到的是哪个节点正在跑,而不是一个转圈。

结果

对话可以跨重启恢复,而同一套持久化还带来了可回放性 —— 我能重建出当时走了哪条路由、哪些文档通过了评分、评分器都说了什么,这既是运维属性,也同样是一种审计属性。

已接受的取舍 PostgreSQL 变成了一个在导入时就确立的硬依赖 —— 没有数据库 URL,图模块一加载就抛错,这让单元测试和评估要么得有一个数据库,要么得小心地做导入隔离。这是一处实打实的摩擦,我会通过把图放进工厂函数里构造、而不是在模块作用域里构造来修掉它。另外,检查点里的状态是运维存储,不是审计记录:既不是不可变的,也没有留存策略。

薄弱点,提前备好

这一页上点到的每一个缺口,集中在一处,附上你打算怎么办。主动交代它们是加分;被人抓出来则不是。

我会自己主动提出来的五个

  • 评估还没进持续集成。 仓库里唯一的自动化是部署。这是清单上价值最高的缺口,大约一天的工作量:每次提交跑 mock 套件,PR 上跑一小轮真实冒烟,夜间对着一份已提交的基线跑全量。
  • 每一处失败放行都是无声的。 没有计数器,没有告警。系统可以在有据性检查一直报错的情况下照常运行,看上去很健康。
  • 评判模型没被验证过。 我从没在同一批用例上测过有据性评分器和人工判断之间的一致率,所以我说不出它的错误率。
  • 视觉页面筛选器没有测过召回率。 这里其他每一项都有一个数字;唯独那一段没有。做法是把一整份完整报告人工标注出来,再对着它打分。
  • 完全没有任何线上度量。 没有影子模式,没有 A/B 测试,没有漂移检测,也没有对线上答案的抽样复核。

他们最可能挖出来的四点

  • 「你最后一道护栏只是个长度检查。」 对 —— 它是底线,不是内容过滤器,而且刻意做得很笨,笨到不会失效。真正的校验工作在前一层。我会补的是:答案里出现数字时强制要求引用,以及数值合理性区间。
  • 「n = 12 算不上证据。」 同意 —— 那是方向,不是显著性,而且我会在被问之前就说明。任何昂贵的决策之前,先多标一些用例。
  • 「你的缓存既没有过期,也没有按用户隔离。」 没错,而且在银行里,隔离这一点是硬阻塞,不是锦上添花。做法是在存储层做命名空间隔离,并给条目按入库运行打版本戳。
  • 「对于藏在文档里的指令,你没有任何防御。」 没错。检索到的 PDF 文本和我自己的指令并排放在一起,目前是被信任的。对公开披露文件来说风险很低,但对任何客户可以上传的东西来说,这是不可接受的。
怎么把一个缺口讲出来又不显得虚 三段,按这个顺序:先直白地说出它是什么,再说它到现在为止为什么还没咬到你,最后说你会造什么、大概要多少成本。「评估还没进 CI。这套套件本来就已经是闸门的形状了 —— 逐用例报告、非零退出码 —— 所以大约一天工作量,而且是我第一件会做的事。」这是一个对优先级有判断的候选人。「我大概应该多写点测试的」,则是一个在道歉的候选人。

术语表

本页出现的术语。更宽的术语表 —— 检索、向量、有据性、离线与线上评估 —— 在配套那一页 interview_prep_nab.html。

展开全部定义 —— 20 个术语,大白话
STAR
Situation、Task、Action、Result(情境、任务、行动、结果)—— 一种面试作答结构,逼你先讲清背景,再讲技术。
按行分隔的 JSON(JSONL)
一个文件里每行一个完整的 JSON 对象。对测试用例很方便,因为文件可以追加写,而且某一行格式坏了可以按行号报出来,不影响其余部分。
Pickle
Python 把一个对象写进文件、再读回来的格式。它保存函数用的是 引用 —— 模块名加函数名 —— 而不是复制代码,所以给分词器模块改个名,会让所有已经建好的索引全部失效。
猴子补丁 / 替换(monkey-patch)
在运行时替换掉一个函数,而不是改源码。在 A/B 测试台架里用来把重排器换成一个直通实现,好让基线和新配置共用同一条代码路径。
基线(baseline)
当前版本被记录下来的分数,留着让日后的运行可以和它对比。控制台日志里的一个数字不是基线;提交进仓库的文件里的一个数字才是。
回归(regression)
以前能用、现在不能用的东西。评估套件存在的首要目的就是抓住它。
hit@k / 平均排名
正确文档有没有出现在前 k 个结果里,以及它平均排在多靠后。平均排名之所以重要,是因为一份排在第 15 位的文档虽然被找到了,但如果只往下传 8 份,它就等于没用。
TPM —— 每分钟 token 数
以文本量而不是请求数来表达的速率限制。超限返回的是一个错误,而如果台架不给自己控速,这个错误很容易被误读成一个质量结果。
数字密度
一页上数字字符除以非空白字符。一个便宜的确定性代理指标,用来判断「这一页是财务表格而不是叙述文字」。
少样本提示(few-shot prompting)
在提示词里塞进做好的示例 —— 这里是四张页面图配手写的正确抽取结果 —— 让模型从示范里学会所需的输出形状,而不是只靠描述。
函数调用(function calling)
模型的一种能力:模型不返回散文,而是返回一个对指定函数、带类型参数的结构化调用。这里纯粹拿它当作把响应压进固定 schema 的手段,而之所以必须这么做,是因为这家服务商拒绝框架默认的 schema 方式。
温度(temperature)
模型在挑选下一个 token 时用多少随机性。零意味着每次都取最可能的那个 —— 这正是路由和评分想要的,写作则不然。
ReAct 智能体
一个循环:模型在推理和调用工具之间来回切换,用每一次的结果决定下一步,直到拿到答案。
CAGR —— 复合年增长率
把你从首个数值带到末个数值、贯穿整段期间的那个单一年增长率。它把逐年的起伏抹平了。
Reducer(归并规则)
并行分支对同一个状态字段写入时,如何合并的规则。默认是覆盖;这里列表做累积,每家公司的标志用字典合并,这样分支之间不会互相抹掉。
中断 / 恢复(interrupt / resume)
把一张图执行到一半停下来等人,然后从同一个点继续。它之所以是持久的,只是因为状态在每一步之后本来就已经写下来了。
连接池
一组可复用的数据库连接,在请求之间共享,这样每个请求不必自己花代价开一条。
SSE —— 服务器推送事件
在普通 HTTP 之上、从服务器到浏览器的单向流。这里用来显示流水线的哪一步正在跑,而不是显示一个转圈。
失败放行 / 失败拦截(fail open / fail closed)
当检查本身出错时:失败放行让请求通过,失败拦截把它挡下。正确方向逐层不同,而且应该是一个写下来的决策。
影响范围(blast radius)
一样东西坏掉时,会连带坏掉多少。这也是最终闸门不发任何网络请求的理由:一条带依赖的底线,不是底线。