很多做 Codebase RAG 的專案,第一步就急著切 chunk、生 embedding、灌進向量資料庫。但如果連「我查 build_index,它到底在專案的哪個檔案、第幾行」這種確定性的問題都答不準,太早導入語意搜尋只會引入更多雜訊與不可控的幻覺。
在做任何複雜的語意檢索之前,今天我們先在 tools.py 實作第一版確定性文字與路徑搜尋器(Deterministic Search)。
第一版的搜尋器策略很單純,鎖定兩大目標:
build_index 命中 src/build_index.py)。app/tools.py我們在 tools.py 的 CodebaseTools 中加入具體的 search() 實作:
# app/tools.py (擴充)
from pathlib import Path
from typing import List, Dict, Any
from app.codebase import CodebaseAnalyzer
class CodebaseTools:
def __init__(self, analyzer: CodebaseAnalyzer):
self.analyzer = analyzer
def search(self, query: str, limit: int = 5) -> List[Dict[str, Any]]:
"""
在專案中執行確定性的文字與路徑搜尋,回傳具備相對路徑與行號的 Evidence
"""
query_clean = query.strip()
if not query_clean:
return []
results: List[Dict[str, Any]] = []
files = self.analyzer.discover_python_files()
# 1. 先比對檔案路徑
for rel_path in files:
path_str = str(rel_path).replace("\\", "/")
if query_clean.lower() in path_str.lower():
results.append({
"path": path_str,
"start_line": 1,
"end_line": 1,
"kind": "path_match",
"name": query_clean,
"snippet": f"# File matched path: {path_str}"
})
if len(results) >= limit:
return results
# 2. 逐行比對程式碼內容
for rel_path in files:
safe_file = self.analyzer.resolve_safe_path(str(rel_path))
try:
lines = safe_file.read_text(encoding="utf-8").splitlines()
except UnicodeDecodeError:
continue
for idx, line in enumerate(lines, start=1):
if query_clean.lower() in line.lower():
results.append({
"path": str(rel_path).replace("\\", "/"),
"start_line": idx,
"end_line": idx,
"kind": "content_match",
"name": query_clean,
"snippet": line.strip()
})
if len(results) >= limit:
return results
return results
tests/unit/test_search.py在接進 CLI 之前,先寫測試確保搜尋行為符合預期,且行號完全正確:
# tests/unit/test_search.py
from app.codebase import CodebaseAnalyzer
from app.tools import CodebaseTools
def test_search_path_and_content(tmp_path):
repo = tmp_path / "repo"
repo.mkdir()
src = repo / "src"
src.mkdir()
# 建立目標檔案與內容
code_file = src / "build_index.py"
code_file.write_text(
"# Header\n"
"import os\n"
"def run_indexing():\n"
" print('indexing')\n",
encoding="utf-8"
)
analyzer = CodebaseAnalyzer(str(repo))
tools = CodebaseTools(analyzer)
# 1. 測試命中路徑
path_matches = tools.search("build_index", limit=5)
assert len(path_matches) > 0
assert path_matches[0]["kind"] == "path_match"
assert path_matches[0]["path"] == "src/build_index.py"
# 2. 測試命中程式碼文字與行號
content_matches = tools.search("run_indexing", limit=5)
assert len(content_matches) == 1
assert content_matches[0]["kind"] == "content_match"
assert content_matches[0]["start_line"] == 3
assert content_matches[0]["snippet"] == "def run_indexing():"
執行測試確認通過:
uv run pytest tests/unit/test_search.py -v
在 app/cli.py 串接 search 子指令後,我們對目標專案 mobileai-local-rag 進行真實檢索:
uv run python -m app.cli search build_index
終端機回傳的第一筆結果:
{
"path": "src/build_index.py",
"start_line": 1,
"end_line": 1,
"kind": "path_match",
"name": "build_index",
"snippet": "# File matched path: src/build_index.py"
}
若搜尋具體函式文字:
uv run python -m app.cli search COLLECTION_NAME
回傳結果精確抓到了設定檔與行號:
{
"path": "src/rag_common.py",
"start_line": 12,
"end_line": 12,
"kind": "content_match",
"name": "COLLECTION_NAME",
"snippet": "COLLECTION_NAME = \"mobileai_docs\""
}
這個版本雖然還沒有語意理解能力,但它先做到了兩件軟體工程中最關鍵的事:搜尋結果 100% 可重現,且每一筆都附帶精準的相對路徑與行號。
下一篇,我們會讓 read_evidence 工具與此搜尋結果無縫對接,直接根據搜尋回傳的檔案與行號,精準截取前後上下文程式碼!