Conversation
today_picks / 日报 / 周报的候选时间窗从 crawled_at 改为 COALESCE(published_at, crawled_at):3 天前发布、今天才抓到的 旧文不再进「今天」,按原文发布时间归档;published_at 为空的 信源回退按抓取时间计。覆盖 DuckDB 主路径、OLTP fallback 与 repo 两侧共 6 处窗口,主备口径逐条对齐。
新增 budget_guard:分钟/小时/日三窗预算保险丝,按 llm_call_logs 实际 DONE 调用数计数,超限拒绝新付费调用(LlmBudgetExceededError)。 插入点在熔断检查与响应缓存之后——缓存命中(免费)照常放行。 分析链路遇预算拒绝走本地降级(与 CircuitOpenError 同分支), 不消耗分析重试次数;预算拒绝记 BUDGET_REJECTED 审计行与 budget_rejected 指标事件;计数查询失败 fail-open。 默认 0/0/0 全关(opt-in),.env.example 附开启建议。
LLM 降级路径(熔断 / 内容过滤 / 预算超限)把内容写成 ANALYZED 终态、 summary_source='local_fallback' 落库后,全代码库没有消费者会把它捡回来重跑: 重分析资格谓词只认 PENDING、僵死 ANALYZING 和 ERROR,手动分析端点遇到已有 记录直接早退。触发原因消失后内容也起不来,永久停在降级时算出的假分上。
requeue job 每 15 分钟在预算余量 ≥30% 时(fail-closed),把最新分析为 local_fallback 的 ANALYZED 内容重置回 PENDING 让分析队列重新拿真实 LLM 分析——把 #90 的永久降级疤变一过性。四重防护:余量门控、60 分钟 冷却、每轮 50 条最旧优先、已有 ≥2 条 fallback 的不再回收(防确定性 降级在 attempts 归零后无限循环)。 预算闸豁免 daily_report/weekly_digest/monthly_digest 低频核心场景, 防日窗耗尽致日报硬失败;新增 budget_headroom_ok 供 requeue 门控。 .env.example 补圆桌四席数值对照表与收紧路线。
scoring_engine 的百分位门槛只由真实 LLM 评分决定:local_fallback 的 确定性假分(curation 收敛 61-64 窄带)不再拖拽 P70 门槛、挤掉真实 65-70 分内容;全降级批次回退全量计算。降级项仍参与 selected 判定 与展示,由 requeue job 换回真实分析。 summary_source 透传补齐全部构造点:DuckDB 主路径、OLTP fallback、 API 响应 payload(前端徽标的数据源)、周报/月报 digest 路径(P70 同类污染一并修复)。today-picks 卡片显示「本地速览 · 待 AI 复核」 琥珀标记,悬停说明自动恢复机制。P70 排除测试为三组自证明对照 (含守门断言,删掉排除逻辑必红)。
原断言只检查 threshold_used 非空,而「本批分数」和「全局默认」两条路径 都会给出数字,删掉被测的 `or [bd.final_score ...]` 回退分支后测试照样全绿。 改成断言门槛等于本批分数的 P70、且不等于 CONFIG["curation_threshold"], 并要求本批至少选出一条。这条回退分支不是防御性冗余:真实分为空时 _compute_percentile_threshold 返回全局默认 55,而降级内容的 final_score 约为 19.94,全部低于 55,缺了这个分支整批一条都选不出来。 变异验证:删掉该分支后本测试转红(55 != 19.94)。
#90 修复已随 wip-llm-budget-guard 分支落地,条目仍写「待修复」和「回归测试 待补」,与实际状态矛盾。补上状态、落地范围、测试条数与变异验证结论。 顺带把条目里的「结疤」换成具体说法,与 AGENTS.md「中文说明怎么写」一致。
agent 用来隔离 issue 切片的 git worktree 放在仓库内 .worktrees/,否则 主工作区的 git status 会被它污染。不入库。
CI backend-lint 阻塞:`ae59cc4` 里那个列表推导被折成 3 行,但并未超长, ruff format 要求单行。本地此前只跑 ruff check、漏了 format --check, 是 CI 抓出来的。 顺带更正上方注释的因果。原文写「避免 _compute_percentile_threshold 收到 空列表」,但空列表在函数内已被处理;真实原因是空列表会被回退到全局默认 阈值 55,而降级内容的 final_score 普遍低于它,那样整批一条都选不出来。
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What & why
Fixes #90
LLM 降级(熔断 / 内容过滤 / 预算超限)把内容写成
ANALYZED终态、summary_source='local_fallback'落库后,全代码库没有消费者会把它捡回来重跑——触发原因消失了也起不来,内容永久停在降级时算出的假分上,并污染 P70 百分位门槛、挤掉真实 65-70 分内容。本 PR 分三块交付。
一、#90 主体(
2a8e67c/ae59cc4)services/analysis_requeue.py+ scheduler 每 15 分钟调度。预算余量 ≥30%(fail-closed)且降级满 60 分钟后,把local_fallback内容重置回 PENDING 让分析队列重新拿真实分析。四重防护:余量门控、60 分钟冷却、每轮 50 条最旧优先、已有 ≥2 条 fallback 的不再回收。analysis.py把LlmBudgetExceededError纳入降级触发源。预算拒绝刻意不计入analysis_attempts——日窗熔断可能比重试预算活得久,否则整条 backlog 会被烧成永久 ERROR。scoring_engine.py排除local_fallback的确定性假分(curation 收敛 61-64 窄带);全候选皆降级时回退全量分数。summary_source透传补齐:DuckDB 主路径、OLTP fallback、API 响应 payload、周报/月报 digest。daily_report/weekly_digest/monthly_digest不做预算检查,防日窗耗尽致日报硬失败。二、预算熔断闸前置(
5e86605)三窗(分钟/小时/日)保险丝。
config.py默认三窗全 0(opt-in,不改变任何现有部署行为)。.env.example记录圆桌四席的数值分歧与收紧前提。三、今日选题/报告窗口按原文发布时间归档(
0fc73d5)窗口条件从
crawled_at改为COALESCE(published_at, crawled_at),三处口径统一。与 #90 无逻辑依赖,但已被 #90 的查询改动牵连,一并交付以免拆分支。四、独立复核后的修正(
e9fd620/191de4b/84c5c55)test_all_fallback_batch_falls_back_to_full_scores名实不符:原断言只检查threshold_used非空,而「本批分数」和「全局默认」两条路径都给数字,删掉被测的回退分支后测试仍全绿。改为断言门槛等于本批分数的 P70 且不等于全局默认。.gitignore忽略.worktrees/(agent 隔离 issue 切片用)。Area
Verification
pytest tests/test_scoring_engine.py tests/test_analysis_requeue.py tests/test_llm_budget_guard.py→ 32 passedpython scripts/check_layering.py→ 49 个 api/v1 文件通过ruff check(5 个改动文件)→ All checks passednpx tsc --noEmit→ 0 错npx vitest run→ 195/195(13 个测试文件)tests/test_source_url_safety.py依赖真实网络的存量红,已在 main 上跑同一文件确认同样 FAILED,非本 PR 引入llm_call_logs.total_cost全 0),预算闸不触发变异验证
绿灯只证明测试存在,不证明测试有效。两个变异都在隔离 worktree 里施加(不污染开发工作区):
test_p70_threshold_excludes_local_fallback_fake_scores转红or [...]回退分支test_all_fallback_batch_falls_back_to_full_scores转红(修正后)第二条的根因值得记一笔:这个回退分支不是防御性冗余。真实分为空时
_compute_percentile_threshold返回全局默认 55,而降级内容final_score约 19.94 全部低于 55——缺了这个分支,整批一条都选不出来,today-picks 直接空白。原代码注释「避免收到空列表」的因果是错的(空列表在函数内已处理)。Verifier verdict
pass(针对 #90 主体实现)
独立复核由非实现过程方执行,逐项核过:分层检查、ruff、前端类型、接线正确性(
summary_source在analysis对象内,getAnalysis()取的正是item.analysis,徽标能显示)、以及冷却基准(content_items.updated_at有onupdate,60 分钟冷却确实从降级时刻算起)。首轮 verdict = hold,抓到 1 条存活变异与 1 条过期矩阵登记,均已在本 PR 内修正并复验转红。
未覆盖的复核边界:第四块的修正由复核者本人提交,未再经第三方独立复核;前端徽标仍无组件测试(JSX 渲染层无测试基础设施,是 #88 起已登记的已知缺口)。合并前以 CI 五项为准。
Checklist
issue-<number>-<short-slug>; PR 正文写Fixes #90让 issue 随合并关闭Notes for reviewer
≥2 条 fallback 不再回收的判断数的是终身降级次数,没有时间边界——理论上历史数据里有多条 fallback 的内容会被永久排除,而这恰是 [P1][LLM] LLM 降级内容不会再被重新分析:local_fallback 落 ANALYZED 终态后无恢复路径 #90 最想救的陈年数据。实践中难触发(手动端点遇已有记录早退、自动候选谓词只认 PENDING/僵死 ANALYZING/ERROR,内容正常只被分析一次),属潜在风险不是现网 bug。收紧只需给个时间窗,成本很低。config.py三窗默认全 0,需显式配置才启用。.env.example里记了收紧前提——requeue 稳定运行后可收紧至 60/1200/10000 并加 10k 告警线。