建本地驗證管道)
你是不是也遇到過這種情況AI 編程助手幫你寫好了整整一個模塊代碼結(jié)構(gòu)完整、注釋規(guī)范、命名講究你幾乎準備直接提交了。結(jié)果仔細一查發(fā)現(xiàn)它調(diào)用了一個你從沒聽過的 API再順著文檔去找這個 API 三年前就被移除了。或者更隱蔽一點它在 requirements 里“認真”地加了一個依賴PyPI 上確實能看到這個名字但包作者是一周前注冊的用戶。AI Agent 編程工具比如 Claude Code、Cursor已經(jīng)進入日常開發(fā)流程。于是AI 在代碼里的“編造”也從聊天機器人的無害胡說升級成一種直接影響代碼庫質(zhì)量、依賴安全甚至線上穩(wěn)定性的工程風(fēng)險。Ledgerful 正是沖著這個問題來的一個運行在本地、專門捕捉“AI 在代碼里發(fā)明東西”的工具。這篇文章要做三件事第一拆解 AI 代碼幻覺到底長什么樣為什么常規(guī)的語法檢查、code review 和單元測試都很難攔住它第二講解 Ledgerful 這類工具的核心設(shè)計——為什么它把“生成”和“證明”分開以及賬本機制在驗證流程中的作用第三給出一個可以獨立跑起來的最小本地檢測器實現(xiàn)包括依賴核驗、API 存在性檢查和語法檢查并把它接進 pre-commit 和 CI。如果你正在用 AI 編程助手或者正準備在團隊里引入 AI Agent 開發(fā)流程這篇文章值得讀完。讀完之后你會對“AI 生成的代碼能否被信任”有一個更實際的判斷標準也能在自己項目里快速搭出一個初級版本的“幻覺攔截器”。1. AI 代碼幻覺為什么這么快就變成了工程問題1.1 從“內(nèi)容胡說”到“代碼事故”前兩年我們談 AI 幻覺主要談的是文本生成——AI 寫一篇技術(shù)文章可能把不存在的論文引用編得煞有介事。那最多算“內(nèi)容風(fēng)險”你只要不拿去投稿損失有限。但代碼生成把這個問題完全放大了。代碼幻覺一旦進入倉庫它會變成真實的進程、真實的網(wǎng)絡(luò)請求、真實的依賴樹和真實的線上事故。AI Agent 編程的典型流程是你給它一個需求它生成一段或多段代碼然后你 review、測試、合入。問題在于Agent 生成代碼的“置信度”很高它不會在代碼旁邊標注“這段我不確定”。一個沒見過AutoTuner類的模型依然會把它寫得像真的一樣參數(shù)合理、調(diào)用鏈完整、返回類型對得上。從文本上看它和正確的代碼幾乎無法區(qū)分。這里的核心矛盾是AI 生成的代碼外在形態(tài)越來越像人寫的代碼但它的“事實依據(jù)”并沒有被任何東西保證。人類程序員寫錯代碼通常是因為疏忽或理解偏差A(yù)I 幻覺則是一種“自信的編造”——它不是在犯錯而是在生成一個語法正確、結(jié)構(gòu)完整、但外部事實可能完全不存在的東西。這個區(qū)別非常重要因為它決定了我們不能沿用傳統(tǒng)的方式去審查 AI 代碼。1.2 為什么常規(guī)檢查兜不住有人會說項目里有 linter、有編譯、有單元測試、有 code review這些機制加起來難道還攔不住幻覺先說編譯和語法檢查。語法檢查只能保證代碼“能被解釋器讀出來”但幻覺代碼往往是語法完全正確的。你調(diào)用一個不存在的 APIPython 解釋器只有在那行代碼執(zhí)行到的時候才會拋AttributeError如果這個分支在測試里沒覆蓋到它就會安靜地在生產(chǎn)環(huán)境里炸開。再說 code review。它的核心弱點是“自動化偏見”當一段代碼結(jié)構(gòu)完整、命名規(guī)范、注釋說明清晰時reviewer 的警惕性會明顯下降。更麻煩的是很多幻覺代碼表面上的正確性本來就比人類錯誤更難識別——至少人類同事寫錯時會猶猶豫豫AI 生成時卻永遠理直氣壯。你很難對一個“看起來合理的東西”保持足夠的懷疑。最后是單元測試。測試能驗證“這個函數(shù)對給定輸入是否返回預(yù)期輸出”但很難驗證“這個函數(shù)所依賴的外部世界是否真實存在”。import some_made_up_package之后如果代碼路徑根本沒執(zhí)行到測試全部綠了也不會發(fā)現(xiàn)依賴是假的等到部署時包管理器才報錯才是最典型的“幻覺漏網(wǎng)現(xiàn)場”。所以我們需要一個獨立于現(xiàn)有檢查體系的驗證層機制上不假設(shè) AI 生成的內(nèi)容是真的而是用賬本、證據(jù)鏈和自動核驗去確認它的真實性。這正是 Ledgerful 想做的事。2. 先弄清楚AI 在代碼里的“編造”到底長什么樣2.1 五種常見幻覺類型要把幻覺檢測做好先得知道幻覺有哪些形態(tài)。結(jié)合我在項目里見過的案例AI 代碼幻覺大致可以分成五類幻覺類型典型表現(xiàn)為什么難以發(fā)現(xiàn)檢測思路虛構(gòu) API調(diào)用不存在的函數(shù)、類、方法語法正確運行到對應(yīng)代碼行才報錯文檔索引、類型系統(tǒng)、運行時反射核驗虛假依賴添加不存在的包或真實但可疑的包代碼不執(zhí)行到對應(yīng) import 就不會暴露查詢 PyPI 或內(nèi)部包源核對包名與版本過期用法使用已刪除或廢棄的接口舊文檔在網(wǎng)上大量存在模型容易“學(xué)習(xí)”到對照版本化文檔和發(fā)布說明虛假配置寫入不存在的配置項或權(quán)限策略很多服務(wù)會靜默忽略未知配置不報錯用配置 schema 做白名單校驗自洽但錯誤的業(yè)務(wù)邏輯邏輯完整、方向錯誤測試按錯誤的業(yè)務(wù)規(guī)則寫結(jié)果全部通過領(lǐng)域規(guī)則校驗人工復(fù)核五類幻覺有一個共同特征它們都不是“明顯的語法錯誤”而是“語法正確但事實不成立”。這也解釋了為什么傳統(tǒng)工具鏈對它們基本失靈。2.2 一個看起來非常真實的錯誤示例我們用一個假想的例子來感受一下。假設(shè) AI 生成了一段“自動調(diào)參”代碼from future_ml import AutoTuner tuner AutoTuner(modebayesian) result tuner.optimize( datasetiris, target_metricaccuracy, max_trials50, ) print(result.best_params)這段代碼的問題在于future_ml這個包不一定存在AutoTuner類也不一定存在于任何真實包里optimize方法的簽名、best_params屬性都可能是模型“順理成章”編出來的。但從閱讀體驗看它變量命名合理、參數(shù)結(jié)構(gòu)完整、函數(shù)調(diào)用鏈自洽——如果 AI 把它作為“參考實現(xiàn)”給你你可能只會疑惑“我怎么沒用過這個庫”。這才是 AI 代碼幻覺最麻煩的地方它編造的不是一個明顯錯誤的fake_function()而是一整套相互自洽的假象。檢測工具要做的是去驗證這些“合理”背后的“事實”是否真實存在。3. Ledgerful 的核心設(shè)計把“生成”和“證明”分開3.1 賬本到底記什么Ledgerful 這個名字很有意思。Ledger 是“賬本”的意思。它把 AI 生成代碼的過程看成一筆一筆“待記賬的交易”模型生成了什么、來自哪個會話、做了哪些檢查、檢查結(jié)果如何、有沒有人復(fù)核過——這些信息按條目記錄形成一條可追蹤的驗證鏈。這與傳統(tǒng) linter 有本質(zhì)區(qū)別。linter 是“發(fā)現(xiàn)問題就報錯”Ledgerful 是“把問題留在證據(jù)鏈里”。它不追求一次檢查就把所有問題改正而是追求每一段 AI 生成代碼都有據(jù)可查后續(xù)任何人都能回溯這段代碼為什么被信任、依據(jù)是什么。一個賬本條目至少應(yīng)該包含這些字段來源信息哪個 AI 工具、哪次會話、哪個 prompt 生成。驗證項語法、依賴、API、配置、運行行為。驗證證據(jù)檢查器返回的原始輸出比如 PyPI 的 404、AST 解析失敗的行號。狀態(tài)通過、警告、失敗、待人工復(fù)核。人工復(fù)核記錄誰看過、結(jié)果如何、是否合入。3.2 分層驗證策略從工程實踐的角度看幻覺驗證可以分成四個層次L1 靜態(tài)檢查語法解析、格式檢查、AST 結(jié)構(gòu)分析。最快、最便宜但只能抓最表面問題。L2 事實核驗去包索引確認依賴是否存在、去文檔或運行時確認 API 是否存在、用 schema 校驗配置項。這是幻覺檢測的關(guān)鍵層。L3 行為驗證在隔離環(huán)境里運行代碼或測試觀察是否有異常網(wǎng)絡(luò)請求、異常文件寫入等行為。L4 人工復(fù)核模型無法自己證明的領(lǐng)域規(guī)則標記后交給人類工程師確認。Ledgerful 這類本地工具的重點一般放在 L1 和 L2因為它們在本地就能完成大部分自動化不需要頻繁觸發(fā)昂貴的外部模型評測也不需要把代碼交給第三方服務(wù)。3.3 本地優(yōu)先原則“本地工具”是這個項目定位的關(guān)鍵詞。代碼審查本來就應(yīng)當發(fā)生在本地倉庫和 CI 環(huán)境里而不是把整個代碼庫發(fā)給第三方服務(wù)去“審查”。本地優(yōu)先意味著三件事代碼不出倉庫在線核驗只發(fā)送必要信息比如包名所有結(jié)論沉淀為本地可審計的記錄。對于看重代碼安全和企業(yè)合規(guī)的團隊這一點尤其重要。4. 構(gòu)建最小可用版本環(huán)境準備與模塊拆解4.1 環(huán)境準備本文的示例使用 Python 3.10 以上版本因為要用標準庫的ast解析和importlib.metadata。你需要準備一個 git 倉庫最好已經(jīng)初始化。Python 3.10 環(huán)境。pre-commit可選用于本地 Git 鉤子集成。能夠訪問 PyPI 的網(wǎng)絡(luò)環(huán)境用于包核驗或者配置內(nèi)部代理源。為了最大程度降低復(fù)現(xiàn)成本最小版本不引入任何第三方依賴全部使用 Python 標準庫。這也符合工具定位它只是一個本地校驗器不承擔重型分析任務(wù)。4.2 項目結(jié)構(gòu)一個最小可用版本可以這樣組織ledgerful-demo/ ├── ledgerful/ │ ├── __init__.py │ ├── cli.py # 命令行入口接收文件并調(diào)用驗證器 │ ├── collector.py # 獲取 git 暫存區(qū)或指定目錄的 Python 文件 │ ├── verifiers/ │ │ ├── __init__.py │ │ ├── syntax_check.py # L1 靜態(tài)檢查AST 語法解析 │ │ ├── deps_check.py # L2 事實核驗依賴包與 import 核驗 │ │ └── api_check.py # L2 事實核驗API 存在性檢查 │ ├── ledger.py # 生成/更新賬本記錄 │ └── reporters.py # 匯總輸出格式化打印 ├── .pre-commit-hooks.yaml └── pyproject.tomlcollector.py負責(zé)收集“要檢查的文件”verifiers目錄放各層檢查器ledger.py負責(zé)把結(jié)果寫入賬本reporters.py負責(zé)任務(wù)結(jié)束后的匯總打印。這個結(jié)構(gòu)的好處是每個驗證器都可以獨立添加和測試后續(xù)要支持 JS、Go 或者其他語言時只需要新增對應(yīng)驗證器。5. 核心代碼實現(xiàn)一個本地“代碼幻覺檢測器”5.1 命令行入口與 Git 暫存區(qū)采集先寫命令行入口。為了讓它能在 pre-commit 里工作我們支持兩種模式指定文件路徑或者使用--staged參數(shù)讀取 git 暫存區(qū)中的 Python 文件。# ledgerful/cli.py import argparse import pathlib import subprocess import sys from ledgerful.verifiers.syntax_check import syntax_check from ledgerful.verifiers.deps_check import deps_check from ledgerful.verifiers.api_check import api_check from ledgerful.ledger import append_entry from ledgerful.reporters import print_report def get_staged_python_files() - list[str]: 獲取 git 暫存區(qū)中新增、修改、復(fù)制、重命名的 Python 文件。 result subprocess.run( [git, diff, --cached, --name-only, --diff-filterACM], capture_outputTrue, textTrue, checkTrue, ) files [] for line in result.stdout.splitlines(): if line.endswith(.py) and pathlib.Path(line).exists(): files.append(line) return files def run_checks(file_path: str) - dict: 依次運行所有驗證器返回該文件的檢查結(jié)果。 results { file: file_path, checks: [], status: pass, } syntax_result syntax_check(file_path) deps_result deps_check(file_path) api_result api_check(file_path) results[checks] [ {name: syntax, **syntax_result}, {name: deps, **deps_result}, {name: api, **api_result}, ] if any(item[status] fail for item in results[checks]): results[status] fail elif any(item[status] warn for item in results[checks]): results[status] warn return results def main() - int: parser argparse.ArgumentParser( descriptionLedgerful - local AI hallucination checker ) parser.add_argument(targets, nargs*, help要檢查的文件或目錄) parser.add_argument(--staged, actionstore_true, help只檢查 git 暫存區(qū)) args parser.parse_args() targets args.targets if args.staged: targets get_staged_python_files() if not targets: print(No Python files to check.) return 0 all_results [] exit_code 0 for file_path in targets: result run_checks(file_path) all_results.append(result) append_entry(result) if result[status] fail: exit_code 1 print_report(all_results) return exit_code if __name__ __main__: sys.exit(main())這段代碼的邏輯很直接get_staged_python_files調(diào)用 git 命令拿到暫存區(qū)文件run_checks依次執(zhí)行三個驗證器append_entry把結(jié)果寫入賬本最后統(tǒng)一打印。真正容易踩坑的地方是如果你的 repo 里有很多 Python 文件都在暫存區(qū)每個文件都去跑一遍 PyPI 網(wǎng)絡(luò)查詢會非常慢。實際使用中建議把“在線核驗”放到 CI 階段本地只跑語法和靜態(tài)檢查或者為常見的包名做一層緩存。5.2 語法檢查器用 AST 抓最表層的錯誤接下來是語法檢查器。使用ast.parse而非compile是因為ast.parse可以直接定位出錯的行號和列號便于在賬本里保存結(jié)構(gòu)化證據(jù)。# ledgerful/verifiers/syntax_check.py import ast from pathlib import Path def syntax_check(path: str) - dict: 檢查 Python 文件語法是否合法。 try: source Path(path).read_text(encodingutf-8) ast.parse(source, filenamestr(path)) return {status: pass, detail: syntax ok} except SyntaxError as e: return { status: fail, detail: fSyntaxError at line {e.lineno}: {