Skip to content

feat(bookkeeping): 自动记账补齐收入识别,抽取关键词 SSoT - #58

Merged
hinson0 merged 2 commits into
mainfrom
worktree-fix-income
Apr 20, 2026
Merged

hinson0 merged 2 commits into
mainfrom
worktree-fix-income

Conversation

@hinson0

@hinson0 hinson0 commented Apr 20, 2026 •

Copy link
Copy Markdown
Owner

背景

用户输入"发工资 8000"、"银行利息 12.5"、"朋友退我 50"等收入表述时,自动记账会被错判为 expense,截图和 eval 均可复现。原因在 apps/backend/services/silicon.py 三个 prompt 都偏向支出:

函数 原问题
classify_intent 只给了"买咖啡 30 块"一个支出 few-shot,"银行利息 12.5"这种名词短语 + 金额被当 chat
extract_bill prompt 里根本没解释 type 字段该怎么判
extract_bill_from_receipt 明确写"type:通常为 expense",硬编码偏见

改动

1. 建立跨语言关键词 SSoT(packages/shared/src/constants/keywords.json)

分两层:

  • notification(窄,4/6 词):前端 parser.ts 通知正则匹配用
  • semantic(宽,23/12 词):后端 LLM prompt hint 用

分层而非合并的原因:如果把"工资""分红""理财收益"这种 semantic 词加进通知正则,会把"工资代发提醒"之类的银行推广文案误识别为到账。

2. 前后端消费同一份 JSON

  • 前端 apps/mobile/lib/auto-bookkeeping/parser.ts 通过 @coco/shared 导入 NOTIFICATION_INCOME_REGEX / NOTIFICATION_EXPENSE_REGEX
  • 后端 apps/backend/constants/keywords.py 用硬编码相对路径 Path(__file__).resolve().parents[3] / "packages/shared/.../keywords.json" 读取同一字节
  • packages/shared/tsconfig.json 开启 resolveJsonModule

3. 三个 prompt 全面重写

  • classify_intent:加判定规则 + 收入 few-shot(银行利息 / 发工资 / 朋友退我 / 理财收益 / 报销);强调"名词短语 + 金额不需要动词也算 record"
  • extract_bill:用 f-string 注入共享关键词表 + 5 条 few-shot(含"刚收到报销 300 → 其他收入,不是'报销'"这种防 category 越界的示例)
  • extract_bill_from_receipt:移除 "通常为 expense" 硬编码,改为"小票默认 expense,但出现收入关键词时判 income"

两个 extract_bill* 函数共用同一个 _INCOME_KW / _EXPENSE_KW 字面量,解决"两处关键词表不一致"的代码评审建议。

4. 离线 eval 脚手架(apps/backend/tests/test_silicon_eval.py)

  • pytestmark = skipif(os.getenv("RUN_LIVE_EVAL") != "1") 门控,默认 skip 不烧 API token
  • 16 个 case:8 个 intent + 8 个 bill,覆盖收入/支出/歧义/OCR 四类场景
  • 对应修了 tests/conftest.py,live eval 模式下不覆盖 .env 里真 SILICON_API_KEY(原来 conftest 的 setdefault 会屏蔽真 key)

5. 单独 commit 修历史 ruff 错(7c7bc16)

routers/auth.py 三个 pre-existing ruff 错(import 未排序 + 2 个 unused import)挡 CI,autofix 修复。send_sms_code 加 # noqa: F401 因为被 patch("routers.auth.send_sms_code") mock。

验证

  • just cicd-be:47 passed + 16 eval skipped
  • just cicd-fe:173 passed(18 suites)
  • RUN_LIVE_EVAL=1 uv run pytest tests/test_silicon_eval.py::test_bill -vs 实调 Qwen3-8B:8/8 bill case 通过
    • 刚收到报销 300 → category="其他收入" ✅
    • 银行利息 12.5 → category="理财" ✅
    • 我请同事吃饭 200 → type=expense, category="餐饮" ✅

Test plan

  • 后端 ruff / format / pyright / pytest 全绿
  • 前端 lint / typecheck / jest 全绿
  • 离线 bill eval 在真实 LLM 上验证
  • 手动回归:在 app 里依次输入"银行利息 12.5"、"发工资 8000"、"打车 45"、"这周花了多少",观察意图判定和账单卡片的 type/category/颜色

Co-Authored-By: Claude noreply@anthropic.com

hinson0 and others added 2 commits April 20, 2026 16:03
- 排序 import 块
- 删除未使用的 `secrets` import
- `send_sms_code` 加 `# noqa: F401` 保留,因为被 test 用 patch("routers.auth.send_sms_code") mock

与当前 PR 的业务改动(收入识别)不相关,仅为解 CI。

Co-Authored-By: Claude <noreply@anthropic.com>
问题:
- 用户说"发工资 8000"、"银行利息 12.5"、"退款 50" 都会被错判为支出
- 根因是 silicon.py 三个 prompt 都偏向 expense:
  * classify_intent 只有支出 few-shot,收入短语被当 chat
  * extract_bill 没有 type 判定规则
  * extract_bill_from_receipt 写死 "type:通常为 expense"

改动:
- packages/shared/src/constants/keywords.json 作为前后端单一事实源
  * notification 层(窄):前端 parser.ts 通知正则用
  * semantic 层(宽):后端 LLM prompt hint 用
- 前端 parser.ts 改用 @coco/shared 导出的常量
- 后端新增 constants/keywords.py 跨语言读取同一份 JSON
- silicon.py 三个 prompt 都改用共享关键词:
  * classify_intent 补充收入 few-shot,强调名词短语 + 金额也是 record
  * extract_bill 补 type 判定规则 + 5 条 few-shot(含"银行利息""我请同事")
  * extract_bill_from_receipt 移除 "通常为 expense",改为允许 income
- tests/test_silicon_eval.py 离线 eval 脚手架(RUN_LIVE_EVAL=1 门控)
- tests/conftest.py live eval 模式下不覆盖 .env 里的真 SILICON_API_KEY

验证:
- just cicd-be:47 passed + 16 eval skipped
- just cicd-fe:173 tests passed
- RUN_LIVE_EVAL=1 跑 eval:8 个 bill case 实调 Qwen3-8B 全通过

Co-Authored-By: Claude <noreply@anthropic.com>
@hinson0
hinson0 merged commit e441479 into main Apr 20, 2026
2 checks passed
@hinson0
hinson0 deleted the worktree-fix-income branch April 20, 2026 08:05
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant