iT邦幫忙

2026 iThome 鐵人賽

DAY 12
0
AI Engineering

地端 AI 建築學系列 第 12

12 LlamaIndex (1) 跟著官方文件快速總覽

  • 分享至 

  • xImage
  •  

前面幾篇蓋好了地端 AI 建築的地基:03 談 Ollama 這個地端 LLM 的好夥伴,04 - 06 則走了一輪 LangChain 到 LangGraph,這篇開始換一個框架 — LlamaIndex。

如果說 LangChain / LangGraph 的強項是「串流程、串工具、串多代理人」,那 LlamaIndex 就更專注在「串資料」這件事:怎麼把你的文件、資料庫、知識庫餵給 LLM,並且做出好用的檢索(Retrieval)與索引(Indexing),這也是為什麼它常常是做 RAG 應用的第一選擇。


官方文件:Starter Tutorial (Using Local LLMs)

連結:https://developers.llamaindex.ai/python/framework/getting_started/starter_example_local/

這邊參考官方文件的示範的「完全用地端模型」文件,調整為以下組合:

  • LLM:透過 Ollama 跑本地的 ornith-1.5-9b
  • Embedding 模型:透過 Ollama 跑本地的 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 範例

第一個範例是一個能做乘法的簡單 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())

流程拆解一下:

  1. 使用者丟出問題
  2. 問題連同工具的 schema(名稱、docstring、參數)一起送進 LLM
  3. LLM 決定要呼叫 multiply 工具,並且產生參數
  4. 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 就會延續對話,而不是每次都重新開始。

加入 RAG 能力

接下來示範怎麼把「文件搜尋」變成一個工具,串進同一個 Agent 裡。步驟大致是:

  1. 準備資料(教學用 Paul Graham 的文章當範例)
  2. SimpleDirectoryReader 讀資料夾
  3. VectorStoreIndex.from_documents() 建索引
  4. 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 模型版本,卻沒重建索引)。


針對地端 Ollama 開發的補充說明

官方文件給的是「能跑起來」的最小範例,實際在地端環境開發時,還有幾個眉角值得展開講:

先確認 Ollama 服務本身跑得起來

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_windowrequest_timeout 是地端開發的兩個關鍵旋鈕

  • context_window:地端跑模型最大的敵人就是記憶體。手動設定 context_window(例如範例裡的 8000)可以避免 LlamaIndex 用模型的預設最大值去配置記憶體,對資源有限的機器特別重要。

  • request_timeout:地端模型的推論速度通常比雲端 API 慢(尤其是 CPU 推論或吃緊的 GPU),把 timeout 拉長(範例是 360 秒)是很常見的做法,避免程式還沒等到回應就先報錯。

Embedding 也走 Ollama: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 裡(而不是散落在每個 VectorStoreIndexquery_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_windowrequest_timeoutbase_url)跟 embedding 地端化的做法。


上一篇
11 案例二:視覺應用(2)看得懂產品的地端 AI 助手實作
下一篇
13 LlamaIndex (2) 檢索後處理
系列文
地端 AI 建築學14
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言