前面幾篇蓋好了地端 AI 建築的地基:03 談 Ollama 這個地端 LLM 的好夥伴,04 - 06 則走了一輪 LangChain 到 LangGraph,這篇開始換一個框架 — LlamaIndex。
如果說 LangChain / LangGraph 的強項是「串流程、串工具、串多代理人」,那 LlamaIndex 就更專注在「串資料」這件事:怎麼把你的文件、資料庫、知識庫餵給 LLM,並且做出好用的檢索(Retrieval)與索引(Indexing),這也是為什麼它常常是做 RAG 應用的第一選擇。
連結:https://developers.llamaindex.ai/python/framework/getting_started/starter_example_local/
這邊參考官方文件的示範的「完全用地端模型」文件,調整為以下組合:
ornith-1.5-9b
embeddinggemma
官方文件是拿 8B 的 llama3.1 做示範,並提醒機器至少要有 約 16GB RAM,我們這裡换成同量級的 ornith-1.5-9b,資源門檻大致相同,一樣建議以 16GB RAM 為基本門檻來評估。
LlamaIndex 是「核心 + 一堆整合套件」的架構,官方文件也特別點出這個設計:套件名稱基本上就是 import 路徑的翻譯,例如:
pip install llama-index-llms-ollama llama-index-embeddings-ollama
對應到程式碼裡就是:
from llama_index.llms.ollama import Ollama
from llama_index.embeddings.ollama import OllamaEmbedding
這個「套件名 = import 路徑」的慣例很好記,之後要接其他地端整合(例如 vLLM、LM Studio、llama.cpp)時,可以直接照這個規律去猜套件名稱。
第一個範例是一個能做乘法的簡單 Agent,重點是 FunctionAgent 搭配 Ollama 這個 LLM 物件:
import asyncio
from llama_index.core.agent.workflow import FunctionAgent
from llama_index.llms.ollama import Ollama
def multiply(a: float, b: float) -> float:
"""Useful for multiplying two numbers."""
return a * b
agent = FunctionAgent(
tools=[multiply],
llm=Ollama(
model="ornith-1.5-9b",
request_timeout=360.0,
context_window=8000, # 手動設定 context window,控制記憶體用量
),
system_prompt="You are a helpful assistant that can multiply two numbers.",
)
async def main():
response = await agent.run("What is 1234 * 4567?")
print(str(response))
if __name__ == "__main__":
asyncio.run(main())
流程拆解一下:
- 使用者丟出問題
- 問題連同工具的 schema(名稱、docstring、參數)一起送進 LLM
- LLM 決定要呼叫
multiply工具,並且產生參數- Agent 拿到工具回傳結果,整合成最終回答
要讓 Agent 記得之前講過什麼,靠的是 Context 物件:
from llama_index.core.workflow import Context
ctx = Context(agent)
response = await agent.run("My name is Logan", ctx=ctx)
response = await agent.run("What is my name?", ctx=ctx)
只要把同一個 ctx 傳進去,Agent 就會延續對話,而不是每次都重新開始。
接下來示範怎麼把「文件搜尋」變成一個工具,串進同一個 Agent 裡。步驟大致是:
- 準備資料(教學用 Paul Graham 的文章當範例)
- 用
SimpleDirectoryReader讀資料夾- 用
VectorStoreIndex.from_documents()建索引- 把
query_engine包成一個 async function,當作工具丟給 Agent
from llama_index.core import VectorStoreIndex, SimpleDirectoryReader, Settings
from llama_index.core.agent.workflow import AgentWorkflow
from llama_index.llms.ollama import Ollama
from llama_index.embeddings.ollama import OllamaEmbedding
import asyncio
# Settings 是全域預設值
Settings.embed_model = OllamaEmbedding(model_name="embeddinggemma")
Settings.llm = Ollama(model="ornith-1.5-9b", request_timeout=360.0, context_window=8000)
documents = SimpleDirectoryReader("data").load_data()
index = VectorStoreIndex.from_documents(documents)
query_engine = index.as_query_engine()
def multiply(a: float, b: float) -> float:
"""Useful for multiplying two numbers."""
return a * b
async def search_documents(query: str) -> str:
"""Useful for answering questions about the essay."""
response = await query_engine.aquery(query)
return str(response)
agent = AgentWorkflow.from_tools_or_functions(
[multiply, search_documents],
llm=Settings.llm,
system_prompt="You are a helpful assistant that can perform calculations and search through documents.",
)
async def main():
response = await agent.run("What did the author do in college? Also, what's 7 * 8?")
print(response)
if __name__ == "__main__":
asyncio.run(main())
這裡有兩個值得注意的設計:
Settings:這是 LlamaIndex 的全域預設值容器,設定一次,後面所有元件(index、query engine)沒特別指定的話都會沿用。這對地端開發特別重要,因為你通常想「整個專案統一用同一顆地端模型」。
Agent 可以同時擁有計算工具跟檢索工具,並自己判斷該用哪一個,這其實就是 RAG 跟 Agent 開始融合的地方。
每次都重新 embedding 很浪費資源,官方也示範了怎麼把索引存到硬碟,之後直接讀回來:
# 儲存
index.storage_context.persist("storage")
# 讀回
from llama_index.core import StorageContext, load_index_from_storage
storage_context = StorageContext.from_defaults(persist_dir="storage")
index = load_index_from_storage(storage_context)
query_engine = index.as_query_engine()
有個細節官方有特別提醒:讀回索引時使用的 embed_model,一定要跟當初建索引時用的是同一顆,不然向量空間對不起來,檢索結果會整個跑掉。這在地端開發時很容易踩雷(例如換了 embedding 模型版本,卻沒重建索引)。
官方文件給的是「能跑起來」的最小範例,實際在地端環境開發時,還有幾個眉角值得展開講:
LlamaIndex 的 Ollama LLM 物件本質上是呼叫本機的 Ollama HTTP API(預設 http://localhost:11434)。所以動手寫程式前,先用最土炮的方式確認服務正常:
ollama list # 看看有哪些模型已經下載
ollama run ornith-1.5-9b # 手動測試模型能不能正常回應
如果 Ollama() 物件連不上,通常不是 LlamaIndex 的問題,而是 Ollama 服務沒啟動,或是 port、host 設定不一致(例如你把 Ollama 跑在遠端主機或容器裡,這時 Ollama(base_url=...) 就要手動指定)。
context_window 與 request_timeout 是地端開發的兩個關鍵旋鈕context_window:地端跑模型最大的敵人就是記憶體。手動設定 context_window(例如範例裡的 8000)可以避免 LlamaIndex 用模型的預設最大值去配置記憶體,對資源有限的機器特別重要。
request_timeout:地端模型的推論速度通常比雲端 API 慢(尤其是 CPU 推論或吃緊的 GPU),把 timeout 拉長(範例是 360 秒)是很常見的做法,避免程式還沒等到回應就先報錯。
embeddinggemma跟官方文件(用 HuggingFace 的 BAAI/bge-base-en-v1.5)不同,這裡我們把 embedding 也換成 Ollama 自己提供的 embeddinggemma 模型,讓 LLM 跟 Embedding 走同一套 Ollama 服務:
from llama_index.embeddings.ollama import OllamaEmbedding
Settings.embed_model = OllamaEmbedding(model_name="embeddinggemma")
在跑之前記得先把模型拉下來:
ollama pull embeddinggemma
這樣做的好處是維運上更單純 — 不需要另外管理 HuggingFace 的模型快取、也不用煩惱 PyTorch/CUDA 版本相不相容,Ollama 一個服務就把 LLM 跟 Embedding 都包了。同樣地,資料完全不需要出網,符合企業內部知識庫這類「完全地端化」的需求。
如果之後遇到 Ollama 上還沒有的 embedding 模型,也可以照官方教學的做法改用 llama-index-embeddings-huggingface,兩種方式可以視情況並存。
Settings 統一管理,換模型只改一個地方地端開發常常需要「換模型測試」同一套 RAG 邏輯,今天用 ornith-1.5-9b,明天想試試別顆模型看效果差多少。把 LLM 跟 Embedding 都集中設定在 Settings 裡(而不是散落在每個 VectorStoreIndex、query_engine 呼叫裡),換模型時就只需要改一行,其他程式碼完全不用動。
ornith-1.5-9b 跑一個完整的地端 Agent最後收個尾,把前面拆開講的東西收斂成一個可以直接執行的完整範例 — 同時具備計算工具跟文件檢索能力,並沿用同一顆 ornith-1.5-9b。開始跑之前,先確認這顆模型已經在本機 Ollama 裡(透過 ollama pull ornith-1.5-9b,或自行匯入 Modelfile 的方式)。
import asyncio
from llama_index.core import VectorStoreIndex, SimpleDirectoryReader, Settings
from llama_index.core.agent.workflow import AgentWorkflow
from llama_index.llms.ollama import Ollama
from llama_index.embeddings.ollama import OllamaEmbedding
# 全域設定:LLM 跟 Embedding 統一走地端
Settings.llm = Ollama(
model="ornith-1.5-9b",
request_timeout=360.0,
context_window=8000,
)
Settings.embed_model = OllamaEmbedding(model_name="embeddinggemma")
# 建立 RAG 索引(假設 data/ 資料夾下已經有文件)
documents = SimpleDirectoryReader("data").load_data()
index = VectorStoreIndex.from_documents(documents)
query_engine = index.as_query_engine()
def multiply(a: float, b: float) -> float:
"""用來計算兩個數字的乘積。"""
return a * b
async def search_documents(query: str) -> str:
"""用來查詢 data 資料夾內文件內容的工具。"""
response = await query_engine.aquery(query)
return str(response)
agent = AgentWorkflow.from_tools_or_functions(
[multiply, search_documents],
llm=Settings.llm,
system_prompt="你是一個地端助理,能做乘法計算,也能查詢資料夾內的文件回答問題。",
)
async def main():
response = await agent.run("文件裡提到了什麼重點?另外,1234 乘以 4567 等於多少?")
print(response)
if __name__ == "__main__":
asyncio.run(main())
跟官方範例相比,唯一改動的地方就是 model="ornith-1.5-9b",這正是 Settings / 建構子參數化設計的好處:框架邏輯不變,只換掉模型名稱,就能無縫切換到自己地端訓練或客製化的模型。
這篇跟著官方文件「地端 LLM 版」starter tutorial 走了一輪 LlamaIndex 的基本骨架:Agent + 工具、對話歷史、RAG 索引、持久化儲存,並補上了地端 Ollama 開發時特別要注意的幾個參數(context_window、request_timeout、base_url)跟 embedding 地端化的做法。