iT邦幫忙

2026 iThome 鐵人賽

DAY 8
0
AI Engineering

30 天打造 Codebase Intelligence Agent:從程式碼檢索、結構化索引到變更影響分析實戰系列 第 8

Day 8:第一次讓 Ollama 真正查 Codebase:統一 Evidence 契約與工具調用實測

  • 分享至 

  • xImage
  •  

前幾天我們建立好專案過濾、完成了確定性搜尋,並將查詢結果嚴格鎖定在受測專案中。

今天,我們終於要邁出關鍵的一步:接上本機 Ollama,讓模型真正根據問題自主調用工具

但在讓模型開口說話前,有一個軟體架構問題必須先解決:如果 search_code 回傳一種格式、find_importers 又回傳另一種格式,LLM 的 Prompt 就會充斥著滿滿的例外處理。

核心前提:統一 Evidence 輸出契約

為了讓模型每次拿到工具回傳時都能穩定解析,我們強制讓所有查詢工具都輸出標準的 Evidence 結構:

{
  "path": "src/rag_common.py",
  "start_line": 1,
  "end_line": 10,
  "kind": "source",
  "name": "src/rag_common.py",
  "snippet": "from __future__ import annotations"
}

無論是文字搜尋、語法樹尋找定義,還是查找引用,輸出通通包含四個絕對要素:相對路徑、起訖行號、類型標記、以及真實程式碼片段。沒有模糊空間。

嚴格的工具白名單(Zero-Trust Tool Registry)

為了維持「唯讀安全」,我們在 app/ollama_runner.py 中建立死守的白名單分派器:

# app/ollama_runner.py (工具白名單核心)
TOOL_FUNCTIONS = {
    "search_code": tools.search_code,
    "find_symbol": tools.find_symbol,
    "read_evidence": tools.read_evidence,
    "find_importers": tools.find_importers,
    "read_git_diff": tools.read_git_diff,
}

清單裡**徹底不存在 bashwrite_filerun_command**。安全防護不是靠在 System Prompt 裡哀求模型「請你保持唯讀」,而是「你的世界裡根本沒有任何寫入工具」。

實作 Ollama Tool Call Runner

我們使用標準的 JSON Schema 向 Ollama 宣告工具規格,並處理 LLM 的 Tool Calling 迴圈:

# app/ollama_runner.py 核心邏輯
import json
import urllib.request
from typing import List, Dict, Any

OLLAMA_API_URL = "http://localhost:11434/api/chat"

# 宣告給 Ollama 的工具定義 (Schema)
TOOL_DEFINITIONS = [
    {
        "type": "function",
        "function": {
            "name": "find_importers",
            "description": "找出專案中所有直接 import 指定模組的檔案與行號",
            "parameters": {
                "type": "object",
                "properties": {
                    "module_name": {"type": "string", "description": "被引用的模組名稱,如 rag_common"}
                },
                "required": ["module_name"]
            }
        }
    }
    # 其餘工具依序宣告...
]

def run_agent_turn(prompt: str, model: str = "gemma2:9b") -> str:
    messages = [{"role": "user", "content": prompt}]
    
    # 1. 第一次向 Ollama 發送請求 (帶入工具白名單)
    payload = {
        "model": model,
        "messages": messages,
        "tools": TOOL_DEFINITIONS,
        "stream": False
    }
    
    req = urllib.request.Request(
        OLLAMA_API_URL, 
        data=json.dumps(payload).encode("utf-8"), 
        headers={"Content-Type": "application/json"}
    )
    
    with urllib.request.urlopen(req) as resp:
        res = json.loads(resp.read().decode("utf-8"))
        msg = res["message"]

    # 2. 檢查模型是否選擇了工具呼叫
    tool_calls = msg.get("tool_calls", [])
    if not tool_calls:
        return msg.get("content", "")

    # 3. 嚴格比對白名單並執行
    for call in tool_calls:
        fn_name = call["function"]["name"]
        fn_args = call["function"]["arguments"]

        if fn_name not in TOOL_FUNCTIONS:
            tool_output = {"error": f"Tool '{fn_name}' not allowed."}
        else:
            # 確定性分派
            tool_output = TOOL_FUNCTIONS[fn_name](**fn_args)

        # 4. 把工具真實輸出的 Evidence 放回上下文
        messages.append(msg)
        messages.append({
            "role": "tool",
            "content": json.dumps(tool_output)
        })

    # 5. 請模型整理最終回答
    final_payload = {"model": model, "messages": messages, "stream": False}
    final_req = urllib.request.Request(
        OLLAMA_API_URL, 
        data=json.dumps(final_payload).encode("utf-8"), 
        headers={"Content-Type": "application/json"}
    )
    with urllib.request.urlopen(final_req) as resp:
        final_res = json.loads(resp.read().decode("utf-8"))
        return final_res["message"]["content"]

實際執行驗證

確認本機 Ollama 服務正常運作:

ollama list

確保使用的是支援 Tool Calling 的模型(例如 gemma2:9b 或同等規格):

NAME            ID              SIZE      MODIFIED
gemma2:9b       ff02c3a0a2e3    5.4 GB    2 days ago

mobileai-local-rag 下達自然語言提問:

uv run python -m app.ollama_runner "哪些檔案直接 import rag_common?"

模型產出的終端機回應

經查閱專案索引,共有以下 4 個檔案直接 import 了 rag_common 模組:
1. src/build_index.py(第 4 行)
2. src/chat_reranker.py(第 6 行)
3. src/chat_reranker_guarded.py(第 6 行)
4. src/rag_chat.py(第 10 行)

今天的成果與驗收標準

這不是我們在程式碼裡寫死呼叫 find_importers,而是:

  1. 意圖識別:模型理解問題意圖,自發選擇了 find_importers 工具。
  2. 白名單守門:Runner 通過白名單檢驗,派發給昨天的底層靜態分析函式。
  3. 證據回填:工具抓出具備相對路徑與行號的結構化 Evidence,交回模型。
  4. 結構化整理:模型根據真實證據產出清晰回覆,零幻覺、零通靈

跑測試確認工具白名單全部註冊正確且無任何危險工具:

uv run pytest tests/unit/test_runner.py -q
5 passed in 0.04s

至此,我們成功完成了從「自然語言提問」到「本機確定性程式碼證據」的完整閉環!明天,我們將開始強化 AST 解析器,把 Python 複雜的 import ... as ... 別名以及符號定義一網打盡。


上一篇
Day 7:先別急著向量搜尋:基礎文字與路徑搜尋實作
系列文
30 天打造 Codebase Intelligence Agent:從程式碼檢索、結構化索引到變更影響分析實戰8
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言