Yuan的博客
EN

金融 RAG Agent 优化:方法、案例与数据

在 FinanceBench 上搭了一个 Agentic RAG 系统做金融问答。Baseline 打到 0.87 之后,有意思的活才开始:怎么往上推。

项目基本盘

数据集FinanceBench —— 150 道问题,对应 168 份页面级的 10-K / 10-Q / 8-K / earnings release PDF。问题类型从事实查询("3M FY2018 capex")到解读("Pfizer 是否在分拆 Upjohn?")都有。

Baseline 管线

  • 检索:voyage-finance-2 dense(1024d)+ Qdrant BM25 sparse + RRF 融合 + 公司感知的 payload 过滤 + voyage rerank-2
  • Agent:LangGraph 多步骤,Claude sonnet-4-6
  • 评估:自建 GPT-4.1 严格 judge(0 / 0.5 / 1.0,针对 FinanceBench 6 类 truth 做了校准)。没用 RAGAS auto-correctness —— RAGAS 在数值精度上过于宽松(Agent 返回 PDF 原值 $302.6M vs truth $303M;RAGAS 会扣分,但其实 Agent 是对的)。

结果

  • dev(45 题):0.922(41.5/45)
  • test(105 题):0.871(91.5/105)
  • 一共 18 个失败案例(16 个 0.0,2 个 0.5)

单一分数掩盖了失败的形态。我把 18 个案例分开看,实施了 4 个独立的修复(P1 / P0 v3 / P3 / P4),恢复了 10 个,把 test 推到了 ~0.919。剩下 8 个没修 —— 每个都有书面记录的理由。


18 个失败按 6 类划分

类别含义数量代表案例
A 输出纪律Agent 行为:拒答 / 过度推理 / 漏了限定词 / 漏了备选定义1100540 AES 拒绝计算库存周转率
B Judge 过严Agent 给的是 PDF 原值;truth 是 rounded;严格 judge 看到的是字面不匹配304171 MGM AP $302.6M vs truth $303M
C 数据集 truth 错Truth 本身就是错的102419 Pfizer:Agent 回答 "No" 实际上是对的,truth 错
D 问题自相矛盾问题文本自相矛盾101328 PEP:原文说"若未列出则填 0"但 truth 是从 notes 里拿的 $411M
E 检索缺口词汇 gap / chunk 污染102119 JPM TBVPS
F 期间 / 语义日历 vs 财年错位 / "按什么 best" 的解读200460 BBY Agent 按日历年 930→907 vs truth 按财年 982→969

关键洞察18 个里有 11 个(61%)是 Agent 行为问题,不是检索失败。当检索已经做到 dev Hit@5 = 100% / test Hit@5 = 98%,瓶颈已经不在召回 —— 而是在 Agent 怎么用召回来的东西。

这决定了修复优先级:便宜但有效的先做(P1,judge 侧)→ Agent 行为(P0 v3 + P4)→ 检索加固(P3)。


修复 1:先怀疑 judge 再怀疑 agent(恢复 3 题)

触发:04171 MGM 应付账款

问题:MGM Resorts FY2018 年末应付账款(美元百万)。
Truth:$303.00

Agent 的回答:

The year-end FY2018 accounts payable... was $302,578 thousand, or approximately $302.6 million.

严格 judge:

回答给的是 $302.6M,与 truth $303.00M 不匹配;严格抽取要求精确匹配 → 0.0

Agent 没错 —— $302.6M 是 PDF 原值;$303M 是四舍五入后的 truth。差距 0.13%。

同样模式的还有:03473 KO ROA(0.01425 vs 0.01),04980 PEP capex($4.625B vs $4.60B)。

修复:在 LLM judge 之前加确定性的 ±2% 预过滤器

不动 Agent。只是在 bench/judge.py 加一个前置步骤:对 pure_number 类型的 truth,跑一个两阶段确定性检查(相对容忍度 + 按 truth 精度四舍五入);通过 → 直接打 1.0。

def _judge_numeric_prefilter(truth, answer, tol=0.02):
    """裸数字 truth 的两阶段检查:
      1. ±tol 相对匹配(处理 $302.6 ≈ $303)
      2. 把 answer 四舍五入到 truth 的有效精度(处理 0.01425 → 0.01)
    同时尝试 ×1000 单位转换(million/billion)。"""
    truth_nums = _extract_numbers(truth)
    answer_nums = _extract_numbers(answer)
    decimals = _truth_effective_decimals(truth)
    for tv in truth_nums:
        for av in answer_nums:
            for factor in (1.0, 1000.0, 1.0/1000.0):
                scaled = av * factor
                if abs(scaled / tv - 1.0) <= tol:
                    return 1.0, "match"
                if decimals >= 0 and round(scaled, decimals) == round(tv, decimals):
                    return 1.0, "round-match"
    return 0.0, "no match"

两个安全约束:

  1. 只对裸数字 truth 触发 —— 正则把关;含 yes/no、标签或方向词的 truth 会回落到 LLM judge
  2. ±2% 在 dev 集上校准 —— 容忍度更大(比如 ±10%)会放过数量级错误的答案

结果

idTruthAgent
03473 KO ROA0.010.014250.01.0
04171 MGM AP$303M$302.6M0.01.0
04980 PEP capex$4.60B$4.625B0.01.0

收获

3 题,0→1.0,judge 侧代码 ~30 分钟搞定。Agent 一次都没重跑。调 Agent 之前先核对 evaluator —— 直觉是"修 Agent 让它输出更圆整的数字";浪费力气,因为 Agent 本来就对。


修复 2:三种 Agent 病灶 —— 反思 + 反拒答 + 词汇扩展(恢复 4 题完整 + 2 题部分)

Class A 的 11 个失败里出现的三种模式

  1. 漏了问题限定词:问"未来 future";Agent 返回总额
  2. 遇到 hedge 子句提前退出:问题给了"如果 X 不适用,请说明原因"的退路;Agent 不试一下就直接走退路
  3. 漏了备选定义:truth 用 "operating WC";Agent 用 "total WC" —— 两个都是合法的 GAAP,Agent 应当给出双答案

一条 prompt 规则盖不住这三种。加了三个结构化约束。

改动 1:在 orchestrator prompt 里加 Answer Quality Rules

project/rag_agent/prompts.pyget_orchestrator_prompt() 里加上:

Answer Quality Rules:
- 严格遵循问题限定词。如果问题指向某个具体子集
  (例如 "future"、"remaining"、"year-end"、"average"、"net of X"),
  返回那个精确的数量 —— 不要返回相关的总额或同级数字。

- 需要时做算术。如果检索到的数据只给了组件,没给最终数字,
  就动手算,给出一行简短计算过程。例:
    Q 问 "remaining",数据显示 total=$700M,"90% incurred"
      → 答 "remaining = $700M × (100% − 90%) = $70M"

- 在用 hedge 子句之前尝试计算。如果问题给了出口("如果 X 不适用,请说明原因"),
  只有在底层数据真的不存在时才用。如果输入存在,先算;可以附 caveat。
  当 line items 在检索 chunk 里看得见时,不要因为该指标在该行业不常用就拒绝计算。

改动 2:独立的 anti_refusal_check 节点

光靠 prompt 规则盖不住所有拒答。加了一个 LangGraph 节点,当草稿答案匹配到拒答模式("cannot determine"、"data is missing"、"insufficient context")时激活。reviewer 是基于证据的:

def get_anti_refusal_prompt():
    return """You are a strict evidence-grounded reviewer.

A retrieval agent produced a DRAFT answer that REFUSES the user's question.
Verify whether the refusal is correct.

CRITICAL RULES:
1. 只使用 RETRIEVED CONTEXTS 中显式陈述的信息。
2. 不要推测、推断或编造数字。
3. 如果上下文中有直接证据 —— 即便是部分的:
   → 用该证据重写。引用来源 chunk。
   → 用限定词("approximately"、"based on available figures")。
   → 不要拒答;有证据支撑的近似答案优于拒答。
4. 如果上下文中没有直接证据:
   → 确认拒答正确,原样输出草稿。
   → 当数据真的缺失时,拒答就是对的 —— 不要编造。
"""

关键设计:prompt 受约束的 reviewer。它只能基于召回上下文重写,不能编造 —— 避免了"通过幻觉来'修复'拒答"的失效模式。

改动 3:rewrite_query 增加 ambiguity_detected + retrieval_variants

在 query 改写步骤增加两个输出字段:

  • ambiguity_detected + ambiguous_terms:标记歧义词,比如 "working capital"(operating vs total)、"EBITDA"(GAAP vs adjusted)、"best by X",让 Agent 知道要给双答案
  • retrieval_variants:把口语化的查询词("residual value to equity holders")翻译成报表里实际使用的 line-item 词汇

经典案例:01911 MGM 利息覆盖倍数

问题:MGM FY2022 利息覆盖倍数,使用 Adjusted EBIT / 利息支出。
Truth:Adjusted EBIT 为负;覆盖倍数为零。

修改前

The earnings release does not present a standalone Adjusted EBIT line item. The closest proxy is taking Operating Income and adding... [computes 2.4x]

Agent 用 OpInc 当 proxy → 2.4x。Truth 是 0。方向都错了 → 0.0。

P0 v3 修改后

Adjusted EBIT, when properly reconciled from Adjusted EBITDAR through D&A and rent, is negative for FY2022. The coverage ratio is functionally zero or undefined.

Judge:

正确识别 Adjusted EBIT 为负,覆盖倍数为零 → 1.0

Agent 学会了接受"负数"作为合法答案,而不是硬凑一个不难看的 proxy。

其他 P0 恢复案例

id怎么修的
00222 AMD 速动比率0.51.0Baseline 给的是标准 QR 1.73;P0 触发备选定义 → 用 related-party AR 重算 → 1.57 = truth
00605 Ulta 回购0.01.0Baseline 算到 36.5% 但因为 FY 命名不确定而拒答;P0 强制"先算、后加 caveat" → 36.5% 带 FY caveat
00606 Ulta 工资0.0 → 0.0 → 1.0光 P0 不够;+P3 多查询才解锁(见修复 3)

部分恢复

00005 Corning 营运资本(0.5 → 0.5):truth 用的是 operating WC = $831M;Agent 用的是 total WC = $2,278M。我以为 P0 反思会触发双答案覆盖两种定义 —— 但 Agent 输出了两个数($2,278M + $2,821M),两个都是 total-WC 变体,把 operating-WC 公式完全漏了。反思 prompt 确实触发了,但 Agent 没有浮现出对的备选定义。基于 prompt 的反思是尽力而为,不是保证。

00540 AES 库存周转率(0.0 → P0 = 0.0 → +P3 = 0.5):见修复 3。

收获

  • P0 v3 完整恢复了 4 题 + 部分恢复了 2 题 —— 没达到分析文档预测的 9-10 题。基于 prompt 的干预很难预测效果。
  • 独立的 review 节点胜过 prompt 规则:前者是基于证据的二次检查;后者依赖 LLM 自己监督自己。
  • 下一轮迭代:把 ambiguity_detected 烤进 structured output schema,让 Agent 被强制填上双答案字段,而不是被 prompt"请求"填上。

修复 3:把口语化查询翻译成报表词汇(一个关键的协同修复)

触发:P0 单独救不了的"检索贫瘠"问题

00606(Ulta 工资)在 P0 之后还是拒答:"SG&A 数据有,但具体的工资明细没有召回到 —— 无法做方向判断。"

问题不在 Agent 的判断 —— 召回上下文里就没有 SG&A 明细。一次 query "Ulta FY2023 wages as % of net sales" 返回的是 SG&A 总额,不是门店薪酬细项。

改动:在 query 改写中加 retrieval_variants

Retrieval vocabulary expansion (additional output):
- 如果问题使用了日常/概念性词汇,而 SEC 报表很可能用不同术语表达,
  那就输出 1-2 个 "retrieval variants":用金融文档词汇做的语义等价改写。
- 原则:报表用会计 line-item 语言,不用商业直觉语言。
  如果问题用了分析性概念("residual value"、"capital intensity"),
  翻译成报表中实际出现的 line items(利润表 / 资产负债表 / 现金流量表 / 分部 notes)。
- 保守一点:如果问题已经用了标准 line items("net income"、"operating cash flow"),
  就不需要生成 variant。

原始 query 和每个 variant 各跑独立的检索;结果用 RRF 合并。

经典案例:00606 Ulta 工资(P0 + P3 协同)

修改前(以及只有 P0 时):拒答 —— "comparison cannot be completed."

P0 + P3 之后:改写输出 variant "Ulta FY2023 SG&A store payroll deleverage components" → 召回 SG&A 明细 → Agent 回答 "store payroll deleveraged in FY2023, meaning wages as % of net sales increased." → 方向匹配 truth → 1.0

协同修复观察:00540 AES

同样模式。光用 P0 = 0.0。加上 P3,改写把比率拆成两次独立检索("AES FY2022 cost of sales" + "AES FY2022 inventory")→ 利润表的 "Total cost of sales: $10,069M" + 两年库存 → Agent 计算 9.5x = $10,069M / $1,055M

Judge:

答案给了 12.1x(平均库存)和 9.5x(期末库存);truth 是 9.5x。答案把 12.1x 标为首选,所以一个数对了但不是主断言 → 0.5

部分得分。Agent 把期末库存当作次选;truth 用的是期末。修复之间有协同 —— 单一修复的验证子集看不出叠加效果。

收获

P3 的设计原则:报表用会计语言,用户用直觉语言 —— 翻译这个 gap。保守一点:当问题已经用了 line item 名时,不要扩展(会引入噪声)。


修复 4:限定词歧义 —— 救一道题,预防一整类失败

触发:P0 v3 没盖住的限定词歧义

01902 Best Buy "best USA category by top line" 暴露了一个 P0 v3 漏掉的根因模式。P0 v3 的 ambiguity_detected 覆盖了术语歧义(working capital / EBITDA / 财年与日历年)但没覆盖限定词歧义 —— 没指明轴的极值表达。

问题:Best Buy 在 Q2 FY2024 期间美国本土市场按 top line 表现最好的产品类别。
Truth:Entertainment +9% 增长(gaming 驱动)。
Agent baseline:Computing & Mobile Phones(绝对营收,~$3.6B)。
  • 解读 A:"top line" = 绝对营收 → Computing $14B
  • 解读 B:"top line" = 营收增长 → Entertainment +9%

同样的根因模式影响任何带 "best / largest / top / leading / primary / key / main" 但没显式指明轴的问题 —— 这是结构性的修复,不是单点补丁

改动:5 层分层修复(commit 12e51c9

光一条 prompt 规则不够 —— 即便检测到限定词歧义,Agent 在答题时还是会"默默坍缩"到某一个轴。需要从 query 改写 → orchestrator → 扇出 → 聚合器的端到端改造:

文件改动
Aprompts.py: get_rewrite_query_prompt把歧义拆成 (a) 术语 vs (b) 限定词;(b) 对极值词非保守;输出格式 <term> — axes: A | B
Bnodes.py: orchestrator注入 AMBIGUITY NOTE,带 3 条 MUST 子句:覆盖每个轴 / 列出每轴的数值 / 永不默默坍缩
Cprompts.py: get_orchestrator_prompt"尊重歧义注记"规则 —— 在 context 压缩中也能存活
Dprompts.py rule 4按轴扇出:每个轴一个改写问题(最多 3 个),每个都明确点名轴
D'nodes.py: aggregate_answers + get_aggregation_promptambiguousTerms 传给聚合器;rule 8 强制 By <axis A>: ...; By <axis B>: ... 格式

对 01902,扇出会拆成:

  • "Best Buy USA Q2 FY2024 best category by revenue absolute"
  • "Best Buy USA Q2 FY2024 best category by revenue growth"

LangGraph 用 Send() 把每个路由到独立子图 → 独立检索 → 聚合器拼成 "By revenue: Computing & Mobile Phones $14B; By growth: Entertainment +9% gaming-driven."

验证(trace 级,4 个案例)

id效果状态
01902 BBY 最佳类别双答案(营收 + 增长)完整恢复
00460 BBY 门店变化覆盖财年 982→969 + 日历年 930→907⚠️ trace 改善;最终 judge 得分取决于双对冲是否被接受
00005 Corning WC双答案(narrow + operating)⚠️ 已知边界:Agent operating = AR+Inv−AP = $2.8B,不是 truth 的 (CA−cash)−(CL−ShortTermDebt) = $831M;子公式枚举超出本次范围
00222 AMD 速动比率双答案(1.57 + 0.92 cash)✅ 已经是 1.0;commit 说明此案例通过 B/C 路径触发(改写在 metric-vs-relevance 上拆分,不是按轴)—— defense-in-depth 显效

⚠️ Trace 级验证(输出格式检查),不是完整 judge 重跑。后续完整重跑会确认提升。

收获

01902 不是靠"加一条 prompt 规则"修好的 —— 是靠 5 层分层改动跨越 rewrite → orchestrator → 扇出 → 聚合器。三点更深的洞察:

  1. B/C 是 defense-in-depth,不是冗余:00222 的双答案是通过 B/C 路径触发的(metric-vs-relevance),不是按轴扇出。没有任何单层能可靠地抓住所有情况。
  2. 00005 边界揭示了 prompt-only 的极限:双答案覆盖了"该不该给双答案?",但没覆盖"哪个子公式?"。后者需要领域知识库(GAAP 对 WC 各公式的枚举),不是 prompt。
  3. 结构性修复 > 一次性补丁:01902 的根因是"极值词缺轴"。任何 "best/largest/top" 问题都会受益 —— 修复救的是一整类失败,不是一道题

8 个未解决的:心里有方案,但当前不值得做

还剩 8 道题(≤ 0.5)。这一节不是"我不知道怎么修" —— 每一个都有具体的方案。不修的理由如下。

类别 1:数据集侧问题(4 题)—— 不在管线范围

id问题
02419 Pfizer 分拆Truth 错了(Upjohn 已于 2020 年 11 月剥离);Agent 回答 "No" 实际上正确
01328 PEP 重组问题自相矛盾:"如果未列出则填 0"但 truth 是从 notes 拿的 $411M
04458 Netflix EBITDA 利润率定义分歧:full-D&A 56.8% / 只算 PP&E 5.4%;Agent 选了前者
00283 Pfizer Upjohn futureA+B 混合:Agent 算 $70M = $700M × 10%(行为正确),但 truth $77.78M(差 ~10%),被严格判

方案:P5(修数据集 truth 或问题文本)。不在管线范围。生产 RAG 天花板被数据噪声结构性压低 —— 通常 95-97%,不是 100%。

类别 2:财年日历错位 —— 需要公司元数据层(1 题)

`00460` BBY 门店:
   Truth:982 → 969(按财年对齐)
   Agent:930 → 907(按日历年对齐)

Best Buy 财年在 1 月末结束;"Q2 FY2024" 在他们的财年里 = 截至 2023 年 7 月的那个季度。Agent 应该是把它解读成了日历 Q2(2022 / 2023 年 6 月)。

方案:单纯 prompt 工程救不了 —— 需要公司元数据层

company_metadata = {
    "BBY":   {fiscal_year_end_month: 1, ticker: "BBY", industry: "Specialty Retail", ...},
    "AAPL":  {fiscal_year_end_month: 9, ticker: "AAPL", industry: "Tech Hardware", ...},
    ...
}

Agent 流程:query 含期间引用 → 先查 company_metadata 解决财年到日历的映射 → 然后再检索。这是 Bloomberg Terminal / Capital IQ / FactSet 内部维护的那一层。

为什么现在不做:是真正的基础设施工作 —— 数据来源(SEC EDGAR?Compustat?人工?)、schema、Agent 状态整合、维护。估计要 1-2 周,超出本期冲刺。但任何生产级金融问答系统都必须有。

类别 3:P0 反思失火(2 题)—— 方案清楚,ROI 不行

id应该实际
00299 JPM 最低分部 Q1 2021双答案:"Corporate −$473M / 4 个 reportable 中 CB $2,393M"排除 Corporate("不是典型 reportable segment"),回答 CB $2,393M。方向反了。
00790 CVS 是否资本密集 yes/no"是" + caveat(ROA 1.82%)拒答,称数据不足

方案

  • 00299:把反思从"建议"改成强制 structured output —— 加一个 Pydantic schema 要求 interpretations: List[str](歧义场景至少 2 项)。Agent 不能默默跳过 —— schema 校验会失败。
  • 00790:收紧 yes/no 的反拒答规则 —— 如果召回上下文有任何间接信号(ROA、比率、趋势),要求给出方向性回答 + caveat。只在完全没有相关数字时才允许拒答。

为什么现在不做

  1. 侵入性大:强制 structured output 要动 graph state schema 和最终答案节点;对当前通过的题有回归风险
  2. 副作用:更紧的反拒答可能把"数据真的缺失"的合法拒答变成幻觉
  3. 收益递减:再加反思规则可能恢复 1-2 题但回归 3-4 题;净收益不明

延后到生产数据中类似模式批量出现再做。

类别 4:ROI 不划算 —— P2 架构改动(1 题)

`02119` JPM 假设清算时每股价值:
   Truth:$66.56(= TBVPS)
   Agent:"无法计算;Q1 2021 资产负债表未召回"

两个叠加的问题:

  1. 词汇 gap:问题说 "bankrupt / liquidate / per shareholder";truth chunk 说 "Tangible Book Value Per Share (TBVPS) = $66.56"。P3 retrieval_variants 没桥接到这个具体的 gap。
  2. 表格碎片化污染:JPM 2021 Q1 10-Q 在 p003 的分部表被切成 9 个 chunk,主导了 top-10 候选。包含 TBVPS 的完整 p006 chunk 被挤了出去。

方案:P2 —— 表格感知切块 + Auto-promote

  • 表格感知切块(接入侧):识别 markdown 表格结构;按逻辑行组切(不按字符数);每个 chunk 都复制表头让它自包含可读。
  • Auto-promote(检索侧):rerank 之后,如果 top-K 多个 chunk 共享同一个 parent_id(≥ N 次),自动合并为完整的父 chunk,避免碎片化占位。

工程量:

  • 重写 project/document_chunker.py(表格感知逻辑)
  • project/rag_agent/tools.py: _search_child_chunks(auto-promote 逻辑)
  • 重处理全部 168 份 PDF + 重建 Qdrant 索引(子 chunk schema 改变)
  • 影响面大:chunk 分布变化可能让当前通过的题回归

为什么现在不做:~16h + 重索引,只恢复 1 题,对 17 题通过的有回归风险。教科书级 ROI 不划算。

但要说清楚:这不是"设计不好" —— 是"当前样本量不撑得起这个改动"。如果生产环境中表格碎片化失败大规模出现(比如 >5 题),P2 立刻就变得值得做 —— 它改善的是全局检索质量(chunk 污染更少、上下文更完整),不是单单这一题。


收尾:4 个收获 + 不修的逻辑

4 个收获

1. 先怀疑 evaluator,再怀疑模型。P1 是 30 分钟 judge 代码;3 题 0→1.0;Agent 一次都没重跑。直觉是"修 Agent 让它输出更圆整的数字" —— 浪费力气,因为 Agent 本来就对。核对 judge 比换更大的模型更重要。

2. Prompt 干预是尽力而为;结构化修复才可靠。P0 v3 预测能恢复 9-10 题;实际是 4 题完整 + 2 题部分。反思 prompt 触发不稳 —— 同一道题可能这次走了 ambiguity 分支,下次就坍缩到单答案。要可靠,就把歧义推到 structured output schema 里强制Agent 填字段。P4 的 5 层分层修复延续了这个原则:query 改写 + orchestrator MUST 子句 + answer-quality 规则 + 扇出 + 聚合器格式约束,端到端 defense-in-depth。

3. 修复之间有协同 —— 单点验证不够00606 只用 P0 没恢复;+P3 才到 1.0。00540 类似。单点验证子集会漏掉叠加效果。在叠加管线上做验证,不是孤立单元测试 —— 后者在多修复系统中会骗你。

4. 失败分类决定优先级 —— 不要无脑打补丁。"61% 是 Agent 行为"直接把工程精力指向 P0 / P4,而不是检索(已经 Hit@5 = 100%)。没有这个分类,本能反应是继续调 reranker —— 可能浪费一周。在动手之前先理解失败分布,是深度复盘最大的回报。

取舍:4 个不修的理由

工程不是"修一切" —— 是知道什么时候停下来。8 个不修的归入 4 类:

类别数量此刻不修的理由
数据集侧(truth 错 / 问题矛盾 / 定义分歧)4不在管线范围;生产 RAG 天花板约 ~95-97%
缺基础设施(公司元数据层)11-2 周真正的基建工作;Bloomberg / Capital IQ 内部维护的那一层;超出本期冲刺,但生产级必备
架构改动(表格感知切块 + 重索引)116h + 重索引 + 回归风险;在当前样本量下为一题不值
Agent prompt 收益递减2每加一条反思规则可能让其他题回归;净收益不明

判断不是"这个改动好不好" —— 而是**"在当前样本量和时间预算下值不值得做"**。同样的 P2 改动:18 题样本下不值得,生产规模下 5% 类似失败时就必须做。做出这个判断比知道怎么修更难。

为什么这种复盘值得写

"我把准确率从 0.871 推到了 0.919"是任何人都能背的数字。把 18 个失败一个个讲清楚、做了什么改动、以及为什么 8 个没修 —— 这才叫主人翁意识。

如果一定要从这个项目里挑一件事,我会指 P4。它没有救一道题 —— 它解决的是一整类失败("极值词缺轴"),生产里会反复遇到。救一题,预防一类 —— 这才是工程值得做的地方。