Files
pp-qa-km/AGENTS.md
LittleYellow 0b4c2bbb15 修復中文路徑 preflight 與 BM25 核心詞落空,測資改照現實建
兩個 bug 同源於自檢測資「照能過建、不照現實建」:

- ingest preflight:git status --porcelain 預設會把含中文或空格的路徑加引號
  轉義,raw/ 白名單比對因此失效,中文檔名的 convert 產出被誤判為 raw/ 以外的
  未提交變更而中止攝入。舊測資用 ASCII 檔名,整條路徑從未被走過。

- search:BM25Okapi 的 idf 在 df >= N/2 時 <= 0(rank_bm25 對負 idf 的替代值
  epsilon x average_idf 在 average_idf 為負時同樣為負),與 search() 的 s > 0
  過濾相乘,會讓「每頁都提到的核心詞」查詢全數落空——知識庫愈小、詞愈核心
  愈嚴重,MCP 的 search_wiki 一併受害。改用 Lucene 式恆正 idf。
  舊測資每個查詢詞都只出現在一頁(df=1),恰好避開此配置。

變更:
- tools/ingest.py:preflight 比對前剝除 git 的轉義引號
- tools/search.py:_BM25 子類覆寫 _calc_idf 為 log(1 + (N-df+0.5)/(df+0.5))
- tools/selfcheck_ingest.py:新增 preflight 中文檔名放行 / 非 raw 擋下的檢查
- tools/selfcheck_search.py:新增與既有頁共用核心詞的測資,鎖住 idf 退化
- tools/convert/selfcheck.py、tools/selfcheck_lint.py:測資檔名改中文+空格
- AGENTS.md §13.4:新增「測資照現實建,不是照能過建」規則
- README.md §3.2:補 convert 產出應保持未提交、由 ingest 一併 commit 的流程

驗證:四個自檢全數通過;分別移除兩個修法後對應檢查會失敗。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-22 07:21:45 +08:00

300 lines
15 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# AGENTS.md — QA 知識庫 schema 正本
> 本檔是本專案**唯一**的 agent 規範正本。`CLAUDE.md` 與 `.github/copilot-instructions.md`
> 只是指向本檔的薄指標檔,禁止在任何其他檔案複製本檔內容(見 §11 雙棧規則)。
## 0. 專案定位
為金融業電子支付QA 測試部門建置的部門知識庫,採 **Karpathy LLM Wiki 模式**
知識在文件**攝入時**由 LLM 編譯成 wiki 頁面(萃取、綜合、交叉引用),查詢時只讀已編譯
頁面。**這不是 RAG**——第一版不建向量庫、不做 chunk 檢索,檢索用 BM25 / 全文檢索。
最終透過一層薄的 MCP server`mcp/server.py`)供團隊成員與內部 AI agents 查詢。
核心哲學:**知識編譯一次、持續維護,而非每次查詢重新推導。**
## 1. 硬性約束(不可協商,違反即為缺陷)
1. **資料落地**:受 FSC 法遵限制,所有文件內容的 LLM 處理一律走本地 Ollama
任何情況下不得將文件內容送往雲端 API。格式轉換一律使用本地程式庫
禁止任何雲端轉換服務。**禁止使用任何 `:cloud` 後綴的 Ollama model**(在遠端執行,
內容會離開本機)。
2. **模型設定外部化**:所有 Ollama 連線資訊與 model tags 一律讀取
`config/models.yaml`,不得寫死在程式碼中。程式不接受環境變數以外的其他覆寫來源;
設定缺欄位時報錯,而非使用隱含預設值。
3. **供應鏈限制**:禁止引入任何中國團隊開發或有中國關聯的模型與框架
(含 bge、Qwen、GLM、MiniMax、Jina 系列)。未來若需 embedding預設候選為
EmbeddingGemmaGoogle或 snowflake-arctic-embedSnowflake
4. **raw 層不可變**`raw/` 內的文件只讀不改。原始檔與轉換後 Markdown 皆登記
SHA-256 至 `raw/manifest.json`(結構見 §9轉換檔條目必須回指原始檔 hash。
wiki 頁面引用來源必須帶 `source_ref`(格式見 §3.3)。
5. **HITL 閘門**:所有 wiki 層的變更以 git branch + PR 形式提交,經人工審核後才
合併至 main。攝入腳本永遠不直接 commit 到 main。lint 絕不擅自刪檔,一律標記
待人工核准。
6. **Copilot 成本紀律**Copilot agents 是薄編排層——文件內容的 LLM 處理一律委派給
本地 Ollama 腳本shell out 至 `tools/`Copilot 只讀取腳本回傳的精簡結果
(狀態、統計、報告路徑),不將文件全文拉進 context。
7. **最小詮釋原則**:遇到模糊需求時採取最小合理詮釋,並將所有假設以
`<!-- ASSUMPTION: ... -->` 註解標註在產出物內,不擅自擴大範圍。
8. **語言**wiki 內容與註解以繁體中文為主;程式碼、識別字、專有名詞用英文。
## 2. 目錄結構
```
pp-qa-knowledge/
├── AGENTS.md # 本檔schema 正本
├── CLAUDE.md # 指標檔 → AGENTS.md
├── .github/
│ ├── copilot-instructions.md # 指標檔 → AGENTS.md
│ └── agents/ # Copilot Custom AgentsPhase 5
├── config/models.yaml # Ollama 端點與 model tags唯一設定來源
├── raw/
│ ├── originals/ # 原始檔docx/xlsx/pdf/pptx/html 快照),只讀
│ ├── converted/ # 轉換後 Markdown攝入管線的實際輸入只讀
│ └── manifest.json # SHA-256 登記 + 原始檔↔轉換檔對應
├── wiki/
│ ├── summaries/ # 每份來源一頁摘要
│ ├── entities/ # 實體頁系統、模組、API、法規條目、專案
│ └── concepts/ # 概念頁:跨來源綜合
├── index.md # 目錄式導航:每頁一行
├── log.md # append-only 操作日誌
├── tools/ # convert/Phase 2、ingest.pyPhase 3
│ # lint.pyPhase 4、search.pyPhase 5
├── mcp/server.py # FastMCP 薄殼Phase 5
└── .gitea/workflows/ # lint 定時排程Phase 4
```
## 3. 頁面類型與 frontmatter schema
### 3.1 三種頁面類型
| type | 位置 | 用途 | 一份來源對應 |
|-----------|-------------------|--------------------------------------------|--------------|
| `summary` | `wiki/summaries/` | 每份來源文件一頁摘要 | 1:1 |
| `entity` | `wiki/entities/` | 系統、模組、API、法規條目、專案等具名實體 | n:m |
| `concept` | `wiki/concepts/` | 跨來源綜合:缺陷模式、測試策略、法遵準則 | n:m |
### 3.2 YAML frontmatter所有欄位必填lint 檢查)
```yaml
---
type: entity # summary | entity | concept
title: "支付閘道" # 頁面標題,繁體中文
description: "一句話摘要,會同步至 index.md 的條目"
tags: [payment, gateway] # 小寫英文 kebab-case至少 1 個
timestamp: 2026-07-14 # 最後「實質內容」更新日 YYYY-MM-DD改錯字不算
sources: # source_ref 列表,至少 1 筆
- "raw/converted/2026-q2-release-report.md#a1b2c3d4"
status: draft # draft攝入產出| reviewed人工審過| stalelint 標記過期)
---
```
<!-- ASSUMPTION: status 生命週期為 draft →PR 審核合併後人工改reviewed →lint 偵測過期stale
ingest 產出一律 draftreviewed 只能由人工設定。 -->
### 3.3 source_ref 格式
```
<repo 相對路徑>#<sha256 前 8 碼>
```
例:`raw/converted/aml-test-guideline.md#3f9c01ab`。hash 以 `raw/manifest.json`
登記值為準;引用原始檔(罕見,如 needs_ocr 的掃描件)同格式指向 `raw/originals/`
### 3.4 檔名規則
wiki 頁檔名用英文 kebab-case slug`wiki/entities/payment-gateway.md`
標題(`title`)用繁體中文。
<!-- ASSUMPTION: 檔名採英文 slug 以符合「識別字用英文」原則,並避免跨平台路徑編碼問題。 -->
## 4. 新頁 vs. 就地編輯
- **開新頁**:內容描述一個獨立實體或概念,且預期會被其他頁連結。
- **就地編輯**:既有實體的屬性更新、補充事實、修正錯誤——更新該頁並在
`sources` 追加新的 source_ref、更新 `timestamp`
- 猶豫時傾向**就地編輯**,避免頁面碎片化;就地編輯後若頁面已涵蓋兩個獨立主題,
才拆分成新頁。
<!-- ASSUMPTION: 「猶豫時傾向就地編輯」為推論的決勝規則,原始需求未明定。 -->
## 5. 內容邊界wiki 化判斷準則)
- **該 wiki 化**(綜合壓縮散落多來源的事實才有價值):跨專案缺陷模式、
需求與測項的追溯關係、法遵測試準則綜合FSC/AML/KYC 相關測試要求)、
歷次結案報告的教訓、部門 SOP 的決策脈絡。
- **不該 wiki 化**(單檔可 grep 的內容做鏡像是負價值):測試案例的逐步驟內容、
單一 API 的規格細節。這些留在 raw 層wiki 只放指向它們的索引條目與跨案例綜合。
## 6. Ingest 工作流
1. **轉換**`tools/convert/convert.py` 將原始檔轉為 Markdown原始檔與轉換檔
皆登記 `raw/manifest.json`(含 SHA-256 與對應關係)。
2. **建分支**`ingest/<YYYYMMDD>-<slug>`,絕不在 main 上工作。
<!-- ASSUMPTION: 分支命名慣例為推論,原始需求只要求 branch + PR。 -->
3. **讀來源**:讀取 `raw/converted/` 下的 Markdown超過
`limits.ingest_chunk_max_chars` 時分段)。
4. **寫 summary**:每份來源產出一頁 `wiki/summaries/`status 為 `draft`
5. **更新 entity / concept**:依 §4 判斷開新頁或就地編輯;單一來源可能觸及多頁。
6. **更新 index.md**:每個新頁 / 改頁同步一行條目(格式見 §10
7. **追加 log.md**:格式見 §10。
8. **開 PR**:推分支至 Gitea remotePR 描述列出:來源檔案 + hash、
新增/修改的頁面清單、LLM 使用的 model tag。人工審核後合併。
## 7. Lint 工作流
`tools/lint.py` 檢查項目:
1. **schema 完整性**frontmatter 欄位齊全、值合法、source_ref 在 manifest 中存在。
2. **過期主張staleness**:頁面 timestamp 過舊、或其 source 對應的原始檔已有
更新版本攝入。
3. **覆蓋缺口**manifest 中已轉換但沒有對應 summary 頁的來源。
4. **孤兒頁**:不被 index.md 或任何其他頁連結的頁面。
5. **重複頁**title / description 高度相似的頁面對。
產出 markdown 報告;**絕不擅自刪檔或改檔**,一律列為「待人工核准」項目。
排程Gitea Actions cronPhase 4
## 8. Query 工作流
1. 先讀 `index.md` 定位候選頁,**最多 10 頁**。
2. 只讀取候選頁內文(透過 `tools/search.py` BM25 排序輔助)。
3. 綜合回答,**必附 source_ref**。
4. **禁止全庫掃描**(遍歷 wiki/ 全部頁面或 raw/ 全文)。
## 9. raw/manifest.json 結構
```jsonc
{
"version": 1,
"files": {
"raw/originals/<檔名>": {
"kind": "original",
"sha256": "<64 hex>",
"media_type": "docx | xlsx | pdf | pptx | html",
"added_at": "<ISO-8601>",
"status": "pending | converted | needs_ocr",
"source_url": "<僅網頁快照:來源 URL>",
"fetched_at": "<僅網頁快照:抓取時間 ISO-8601>"
},
"raw/converted/<檔名>.md": {
"kind": "converted",
"sha256": "<64 hex>",
"original_path": "raw/originals/<檔名>",
"original_sha256": "<64 hex>",
"converter": "from_docx | from_xlsx | from_pdf | from_pptx | from_web",
"converted_at": "<ISO-8601>"
}
}
}
```
<!-- ASSUMPTION: 以路徑為 key 的物件結構(而非陣列)便於查找與去重;
needs_ocr 登記在 original 條目的 status該檔沒有 converted 條目)。 -->
## 10. index.md 與 log.md 格式
**index.md** — 目錄式導航,依頁面類型分節,每頁一行:
```
- [標題](wiki/entities/payment-gateway.md) — 一句摘要 #payment #gateway
```
**log.md** — append-only 操作日誌,新條目追加於檔案末尾,不修改既有條目:
```
## [YYYY-MM-DD] <op> | <title>
```
`op``bootstrap` / `ingest` / `edit` / `lint` / `review`
<!-- ASSUMPTION: op 詞彙表為推論,原始需求只定義格式字串。 -->
## 11. 雙棧規則
- 本檔為唯一正本。`CLAUDE.md`Claude Code 入口)與
`.github/copilot-instructions.md`Copilot 入口)只是薄指標檔
(一行說明 + 引用本檔),禁止複製 schema 內容。
- Claude Code 與 Copilot 的行為差異(若有)以「附註」形式標明在本檔相關章節,
禁止另立分叉檔。
- Copilot Custom Agents`.github/agents/*.agent.md`Phase 5遵守 §1.6
成本紀律agent 是編排者不是處理者。
## 12. Meta-loop 紀律
審核者(人工 review PR 或 lint 報告時)發現**系統性**問題——同類錯誤重複出現——
修改的是本 schema 或 lint 規則,而非逐筆修正輸出(**change the ruler, not the
output**)。單筆修正只治標;規則修正才治本。
## 13. Agent 行為紀律
<!-- 併入自原 CLAUDE.mdKarpathy LLM coding-pitfall + Ponytail minimalism ladder
依 Phase 0 決議 Q4(a)。權衡:偏向謹慎與極簡並重;瑣碎任務用判斷力,
答案明顯就不問,直接寫最小可行的東西。 -->
### 13.1 先想再寫Think before coding
不假設、不隱藏困惑、主動揭露權衡。
- 明確陳述假設。
- 詮釋真正分歧時,只問**一個**釐清問題;否則採最小詮釋、以行內註解標註假設、
繼續前進——不要停滯。
- 存在更簡單的做法時要說出來;該反駁就反駁。
### 13.2 寫最少能動的程式碼the ladder
動手前,停在**第一個**能解決問題的階梯:
1. **Skip it** — 這功能真的需要嗎YAGNI
2. **Stdlib** — 標準函式庫是否已提供?
3. **Native** — 平台/執行環境是否內建?
4. **Existing dependency** — 已安裝的依賴能否勝任?
5. **One line** — 能不能一行解決?
6. **Minimum** — 到此才寫最少能動的程式碼。
不做超出要求的功能、不為單次使用寫抽象、不做投機的彈性與可配置性、
不為不可能發生的情境寫錯誤處理。寫了 200 行但 50 行能解決,就重寫。
檢驗:「資深工程師會不會說這寫得過度複雜?」會,就簡化。
### 13.3 不能偷懶的地方
13.2 **不適用**於以下情況——這些要做好做滿:
- 信任邊界的輸入驗證。
- 防止資料遺失的錯誤處理。
- 資安。
- 無障礙accessibility
- 真實硬體需要的校準(時鐘漂移、感測器偏差——平台從來不是規格書上的理想值)。
- 使用者明確要求的任何事。
### 13.4 標記並驗證你的捷徑
沒有檢查的偷懶程式碼是未完成品。
- 每個刻意的簡化都以 `ponytail:` 註解標記;若捷徑有已知天花板
global lock、O(n²) 掃描、naive heuristic註解要寫明天花板與升級路徑。
- 非平凡邏輯要留下**一個**可執行的檢查——邏輯壞掉就會失敗的最小東西
assert 式自檢或一個小測試檔;不用框架、不用 fixtures
- **測資照現實建,不是照「能過」建**:來源檔名用中文+空格(真實 QA 文件就長這樣)、
語料要有跨頁共用的核心詞。便利值會變成盲點的形狀——ASCII 檔名讓 `git status`
的路徑轉義 bug 溜過,互不重疊的語料讓 BM25 的 idf 退化 bug 溜過。
新增檢查時先問:**這組測資和真實輸入差在哪?差異處就是沒被測到的地方。**
- 把任務轉成可驗證的目標,迭代到通過:
- 「加驗證」→ 先寫無效輸入的測試,再讓它通過。
- 「修 bug」→ 先寫重現 bug 的測試,再讓它通過。
- 「重構 X」→ 重構前後測試都要通過。
多步驟任務要先列簡短計畫:`1. 步驟 → verify: 檢查方式`
### 13.5 手術式修改
只碰必須碰的,只清自己的垃圾。
- 不「順手改善」相鄰的程式碼、註解、排版。
- 不重構沒壞的東西;配合既有風格。
- 移除**因你的變更**而失效的 import/變數/函式。
- 既有的死程式碼:提出來,但不刪(除非被要求)。
檢驗:每一行變更都能直接追溯到使用者的要求。
**有效的跡象**diff 裡不必要的變更變少、因過度複雜而重寫的次數變少、
釐清問題發生在動手**之前**而非犯錯之後、刻意的捷徑是可見的(`ponytail:`
而非沉默的。