iT邦幫忙

2026 iThome 鐵人賽

DAY 7
0

很多做 Codebase RAG 的專案,第一步就急著切 chunk、生 embedding、灌進向量資料庫。但如果連「我查 build_index,它到底在專案的哪個檔案、第幾行」這種確定性的問題都答不準,太早導入語意搜尋只會引入更多雜訊與不可控的幻覺。

在做任何複雜的語意檢索之前,今天我們先在 tools.py 實作第一版確定性文字與路徑搜尋器(Deterministic Search)

搜尋邏輯:雙軌比對

第一版的搜尋器策略很單純,鎖定兩大目標:

  1. 路徑比對(Path Match):Query 是否命中了檔案或模組路徑(例如搜尋 build_index 命中 src/build_index.py)。
  2. 文字行比對(Content Match):逐行掃描合規檔案,比對原始碼文字(Case-insensitive substring),精確記錄起始行號與程式碼片段(Snippet)

實作工具層搜尋:擴充 app/tools.py

我們在 tools.pyCodebaseTools 中加入具體的 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

整合 CLI 與實際檢索驗證

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 工具與此搜尋結果無縫對接,直接根據搜尋回傳的檔案與行號,精準截取前後上下文程式碼!



上一篇
Day 6:掃 repository 前先學會閉眼:排除規則與過濾實作
系列文
30 天打造 Codebase Intelligence Agent:從程式碼檢索、結構化索引到變更影響分析實戰7
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言