自動測試整個機器人Testing the whole bot
機器人有了搜尋、格式檢查、工具、記憶。每改一個地方,都可能影響其他地方。這一章把入門篇 B5 的「考卷」升級成兩種測試,讓你每次修改後幾秒內就知道有沒有弄壞東西。
The bot now has search, validation, tools and memory, and changing one part can affect the others. This chapter upgrades the B5 "exam" into two kinds of test that tell you within seconds whether a change broke something.
這章要加的功能What this chapter adds
- 單元測試:用 pytest 檢查每個零件(搜尋、格式檢查、工具、迴圈上限)。
- 評測集:18 題真實問題,逐題檢查四件事。
- 基準:分數變差就失敗,放進 CI 自動執行。
- Unit tests: pytest checks each part (search, validation, tools, loop limits).
- An eval set: 18 realistic questions, each checked four ways.
- A baseline: fail when the score drops, run automatically in CI.
兩種測試,回答兩種問題Two kinds of test, two questions
程式的每個零件對不對?Is each part of the code right?
結果固定(用假模型)、跑得很快、每次修改都跑。例如:捏造出處時會不會重試?工具迴圈會不會停?
Deterministic (fake model), fast, run on every change. For example: does a fabricated citation trigger a retry? Does the tool loop stop?
整個機器人回答得好不好?Does the whole bot answer well?
用一組真實問題逐題檢查。用假模型時檢查流程;用真模型時檢查回答品質。
Checks a set of realistic questions one by one. With the fake it checks the pipeline; with the real model it checks answer quality.
單元測試:測「會出事」的地方Unit tests: test what can go wrong
好的單元測試不是只測「正常情況會成功」,而是測那些出事時會很嚴重的地方。以下是 tests/test_intermediate.py 的幾個例子:
Good unit tests don't just check the happy path; they target what would be serious if it broke. Some examples from tests/test_intermediate.py:
def test_tools_take_no_employee_id():
# 權限:沒有任何工具能讓 AI 指定要查誰
for tool in TOOLS:
assert "employee_id" not in tool["input_schema"]["properties"]
def test_retry_fixes_fake_citation():
# 第一次故意捏造出處,確認會重試並修正
t = HandbookBot(FakeLLM(mistakes=["fake_citation"])).ask("事假一年可以請幾天?")
assert t.attempts == 2 and t.answer.citations == ["H03"]
class LoopingLLM:
"""永遠要求用工具的模型,用來測試迴圈上限。"""
def create(self, system, messages, tools=None, output_schema=None):
call = ToolCall("t1", "get_today", {})
return Reply([...], "", [call], "tool_use", 10, 10)
def test_tool_rounds_are_capped():
# 成本:模型一直要工具時,迴圈一定會停
t = HandbookBot(LoopingLLM()).ask("今天幾號?")
assert "超過" in t.error and len(t.tool_calls) == config.MAX_TOOL_ROUNDS
注意 LoopingLLM:為了測一個很少發生、但發生了很貴的情況,我們寫了一個「故意壞掉」的模型。這就是第 1 章把 LLM 包成介面的回報。
Note LoopingLLM: to test a rare but expensive situation, we wrote a deliberately broken model. That's the payoff from wrapping the LLM in an interface in chapter I1.
評測集:每題檢查四件事The eval set: four checks per question
{"id": "wfh-synonym", "question": "一週可以在家上班幾天?", "relevant": ["H05"], "must_include": ["兩天"], "tags": ["synonym"]}
{"id": "no-parking", "question": "公司有員工停車位嗎?", "relevant": [], "expect_not_found": true, "tags": ["not-in-handbook"]}
{"id": "tool-balance", "question": "我今年還剩幾天特休?", "relevant": ["H01"], "must_include": ["4"], "expect_tool": "get_leave_balance", "tags": ["tool"]}
| 檢查Check | 內容What it verifies |
|---|---|
| 搜尋Search | 相關段落有沒有被找到;手冊沒寫的題目,應該什麼都找不到The relevant passage was found; for not-in-handbook questions, nothing should be found |
| 內容Content | 答案包含關鍵字(例如「兩天」)The answer contains the key fact (e.g. 兩天) |
| 誠實Honesty | 手冊沒寫的題目,found 必須是 falseFor not-in-handbook questions, found must be false |
| 工具Tools | 該用工具的題目,有沒有真的呼叫Questions that need a tool actually called it |
✓ annual-notice 請特休要提前幾天申請?
✗ annual-synonym 年假要提前多久講?
搜尋:找到 [],應該是 ['H01']
內容:答案沒有「三個工作天」
✗ core-hours 最晚幾點要到公司?
搜尋:找到 [],應該是 ['H04']
內容:答案沒有「十點」
✓ laptop-synonym 筆電壞了可以自己拿去修嗎?
✓ no-parking 公司有員工停車位嗎?
✓ tool-balance 我今年還剩幾天特休?
...
{"cases": 18, "passed": 15, "pass_rate": 0.8333}
失敗訊息直接指出是哪一步出問題:這兩題都是「搜尋」沒找到,所以要改的是搜尋(第 3 章),不是 prompt。分開檢查每一步,才知道該修哪裡。
The failure messages point straight at the broken step: both failed at search, so the fix belongs in search (chapter I3), not the prompt. Checking each step separately tells you where to look.
基準與 CIBaseline and CI
確認結果合理後,執行 --update-baseline 把分數存成基準。之後只要分數低於基準,evals.py 就會以失敗結束,CI 會擋下這次修改。這個專案的 GitHub Actions 已經設定好,每次推送都會跑全部測試。
Once the results look right, run --update-baseline to save the score. From then on, a lower score makes evals.py exit with failure and CI blocks the change. This project's GitHub Actions already runs every test on each push.
python -m pytest labs/intermediate # 單元測試
python -m labs.intermediate.evals # 評測(假模型),比較基準
python -m labs.intermediate.evals --update-baseline # 人確認過後才更新基準
python -m labs.intermediate.evals --real # 真模型(要 API key、會花錢、分數會浮動)
python -m labs.intermediate.evals --ollama # 本機開源模型(免費但較慢,見第 08 章)
假模型與真模型的分工Fake vs. real model
| 假模型Fake | 真模型Real | |
|---|---|---|
| 驗證Verifies | 流程:搜尋、工具、檢查、重試The pipeline: search, tools, checks, retries | 回答品質Answer quality |
| 成本Cost | 免費Free | 每次都要付費Paid every run |
| 結果Results | 每次相同Identical every time | 會浮動,要看趨勢Vary; look at trends |
| 什麼時候跑When | 每次修改(CI)Every change (CI) | 改 prompt、換模型、上線前Prompt changes, model switches, before launch |
中級篇完成了You've finished the intermediate track
下一章 I7 會把它部署成別人能用的網站。到這裡,你做出了一個完整的小型 AI 應用:會搜尋、會檢查自己的回答、會用工具、記得對話、知道花了多少錢,而且每個部分都有測試。這已經是很多公司內部工具的實際樣貌。
The next chapter, I7, deploys it as a website others can use. At this point you've built a complete small AI application: it searches, checks its own answers, uses tools, remembers the conversation, knows what it costs, and every part is tested. That's what many real internal tools actually look like.
進階篇處理的是「把這種系統放到客戶的正式環境」時會遇到的問題:
The core course tackles what happens when you put systems like this into a customer's production environment:
| 中級篇做到Intermediate covered | 進階篇延伸Core extends to |
|---|---|
| I2 出處檢查與重試I2 Citation checks and retries | 04 各家 API 的限制、schema 設計04 Provider limits, schema design |
| I3 關鍵字搜尋與同義詞I3 Keyword search and synonyms | 05 向量搜尋、混合檢索、權限過濾05 Vector search, hybrid retrieval, permissions |
| I4 唯讀工具I4 Read-only tools | 06 會改資料的工具、人工核准、狀態保存06 Data-changing tools, human approval, durable state |
| I5 用量與費用I5 Usage and cost | 08 監控、提示注入、可靠性、分階段上線08 Monitoring, prompt injection, reliability, staged rollout |
| I6 評測與基準I6 Evals and baselines | 07 從真實失敗建測試集、LLM 評審、雜訊07 Test sets from real failures, LLM judges, noise |
你已經有能力完成作品集專案的題目 1 到 4。把這個機器人換成你自己的資料,就是一個很好的開始。
You can already tackle portfolio projects 1–4. Swapping your own data into this bot is a great start.