如何基于自定義MCP服務器構建支持工具調用的Llama智能體(含code)
一、背景與目標:從知識隔離到本地化智能體
在人工智能應用日益普及的今天,隱私保護與數據主權成為重要挑戰。傳統的AI模型依賴外部服務,導致私有知識面臨泄露風險。本文將詳細介紹如何構建一個完全本地化的AI智能體,通過自定義的Model Context Protocol(MCP)服務器實現知識隔離,并結合Llama 3.2輕量級模型實現工具調用能力。這一方案不僅確保數據完全在本地運行,還能通過工具調用與私有知識庫深度交互,為本地化智能應用提供了可行路徑。
(一)MCP服務器的前世今生
在之前的文章中,筆者構建了一個自定義MCP服務器,其核心目標是實現三大功能:
- 只讀訪問控制避免AI模型對文件系統進行寫入操作,防止數據篡改。
- 路徑隱私保護隱藏文件目錄結構,僅向模型暴露必要的知識內容。
- 協議深度理解通過自主實現MCP協議,深入掌握其工作原理。
該服務器通過標準輸入輸出(stdio)與外部通信,能夠連接Obsidian知識庫,提供工具調用接口。例如,list_knowledges
工具用于獲取知識庫中的文件列表,get_knowledge_by_uri
工具通過URI檢索具體知識內容。這些工具為后續智能體的構建奠定了基礎。
(二)從外部模型到本地化的挑戰
盡管現有MCP服務器已實現知識隔離,但依賴外部AI模型(如Claude)仍存在兩大問題:
- 成本限制Claude等服務需付費訂閱,免費版本功能受限。
- 隱私風險私有知識需傳輸至外部服務器,存在泄露隱患。
因此,構建完全本地化的智能體成為必然選擇。核心目標包括:
- 實現MCP客戶端,與自定義服務器通信。
- 集成本地運行的LLM模型,替代外部服務。
- 構建智能體框架,結合MCP工具與LLM實現問答邏輯。
二、核心技術選型:輕量級模型與工具調用機制
(一)Llama 3.2模型的選擇
在智能體開發中,語言模型是核心“大腦”。考慮到本地化運行需求,需選擇輕量級且支持工具調用的模型。Llama 3.2系列的1B/3B模型成為理想選擇,其特點包括:
- 設備友好性可在本地GPU/CPU運行,無需云端資源。
- 工具調用支持內置對函數調用的理解能力,符合MCP協議需求。
- 性能平衡通過結構化剪枝與知識蒸餾,在模型大小與推理能力間取得平衡。
Meta官方數據顯示,Llama 3.2 3B模型在保持較小體積的同時,能夠處理復雜指令并生成高質量響應。例如,在工具調用場景中,該模型可解析函數參數并生成正確的調用格式,盡管其多輪對話能力稍遜于70B以上的大型模型。
(二)工具調用流程解析
Llama模型的工具調用基于特定的提示格式與角色機制,核心流程如下:
1.系統提示定義工具在系統提示中以JSON格式聲明可用工具及其參數。
{
"name": "get_user_name",
"description": "Retrieve a name for a specific user by their unique identifier",
"parameters": {
"type": "dict",
"required": ["user_id"],
"properties": {"user_id": {"type": "integer"}}
}
}
2.用戶提問觸發調用用戶問題觸發模型判斷是否需要工具。例如,查詢用戶ID為7890的名稱時,模型生成工具調用表達式[get_user_name(user_id=7890)]
。
3.執行工具并反饋結果應用解析調用表達式,通過MCP客戶端執行工具,將結果(如{"output": "Hyunjong Lee"}
)以ipython
角色返回模型。
4.結果合成響應模型結合工具輸出生成最終回答,如“The name of user who has the ID is Hyunjong Lee”。
需要注意的是,輕量級模型(如3B)在處理多輪工具調用時可能不穩定。Meta建議,對于復雜對話場景,優先使用70B以上模型,但在單輪或簡單多輪調用中,3B模型仍可勝任。
三、智能體架構設計:從客戶端到對話邏輯
智能體的整體架構包含三大核心組件:MCP客戶端與管理器、LLM模型接口、智能體邏輯層。以下是各部分的詳細實現。
(一)MCP客戶端與管理器
1. MCP客戶端實現
使用Python的MCP SDK構建客戶端,通過標準輸入輸出與服務器進程通信。核心類MCPClient
負責連接服務器、初始化會話并執行工具調用:
class MCPClient:
async def connect_to_server(self, server_script_path):
server_params = StdioServerParameters(command="python", args=[server_script_path])
stdio_transport = await self.exit_stack.enter_async_context(stdio_client(server_params))
self.read, self.write = stdio_transport
self.session = await self.exit_stack.enter_async_context(ClientSession(self.read, self.write))
init_result = await self.session.initialize()
self.name = f"{init_result.serverInfo.name}(v{init_result.serverInfo.version})"
async def call_tool(self, name, args):
response = await self.session.call_tool(name, args)
return response.isError, response.content
客戶端遵循MCP握手流程:首先發送初始化請求,獲取服務器信息(如協議版本、工具列表),然后通過list_tools
、list_resources
等方法枚舉可用資源,通過call_tool
執行具體工具。
2. MCP管理器
為支持多服務器管理,設計MCPClientManager
類,負責客戶端實例的創建、銷毀及工具映射:
class MCPClientManager:
def __init__(self):
self.clients = []
self.tool_map = {} # 工具名到客戶端索引的映射
async def init_mcp_client(self, server_paths):
for path in server_paths:
client = MCPClient()
await client.connect_to_server(path)
self.clients.append(client)
tools = await client.list_tools()
for tool in tools:
self.tool_map[tool.name] = len(self.clients) - 1
async def call_tool(self, name, params):
idx = self.tool_map.get(name, -1)
if idx == -1:
raise Exception(f"Tool {name} not found")
return await self.clients[idx].call_tool(name, params)
管理器維護工具與客戶端的映射關系,確保工具調用請求正確路由至對應的服務器實例。
(二)LLM模型集成
1. Llama.cpp的本地化部署
使用Llama.cpp庫在本地運行Llama模型,步驟如下:
- 模型下載從Hugging Face獲取Llama 3.2 3B-Instruct模型權重。
from huggingface_hub import snapshot_download
snapshot_download("meta-llama/Llama-3.2-3B-Instruct", local_dir="./models")
- 格式轉換使用Llama.cpp提供的腳本將模型轉換為GGUF格式,便于高效推理。
python convert_hf_to_gguf.py ./models/Llama-3.2-3B-Instruct --outfile model.gguf --outtype f16
- Python接口封裝:通過自定義類
LlamaCPP
包裝Llama.cpp的推理接口,支持提示生成與響應解析。
class LlamaCPP:
def __init__(self, model_path):
self.model = Llama(model_path=model_path, n_ctx=1024)
def generate(self, prompt, max_tokens=512):
output = self.model(prompt, max_tokens=max_tokens)
return output["choices"][0]["text"].strip()
2. 提示工程與對話歷史管理
為適配Llama的提示格式,設計LlamaMessage
與LlamaPrompt
類,負責消息格式化與對話歷史維護:
class LlamaMessage:
def __init__(self, role, cnotallow="", tool_scheme=""):
self.role = role
self.content = content
self.tool_scheme = tool_scheme
def template(self, tool_enabled=False):
prompt = f"<|start_header_id|>{self.role}<|end_header_id|>"
if tool_enabled and self.tool_scheme:
prompt += self.tool_scheme
if self.content:
prompt += f"{self.content}<|eot_id|>"
return prompt
class LlamaPrompt:
def __init__(self):
self.system_prompt = LlamaMessage("system", "You are a helpful assistant.")
self.history = History()
def get_generation_prompt(self, tool_enabled=False, last=50):
prompt = [self.system_prompt] + self.history.get_chat_history(last) + [LlamaMessage("assistant")]
return ''.join([msg.template(tool_enabled) for msg in prompt])
LlamaPrompt
類支持動態添加用戶、助手、工具結果等角色的消息,并根據tool_enabled
參數決定是否在提示中包含工具調用說明,避免輕量級模型因持續看到工具指令而產生混淆。
(三)智能體核心邏輯
1. 工具調用決策與結果處理
智能體通過正則表達式匹配工具調用模式,解析函數名與參數,并調用對應的MCP工具:
class Agent:
def __init__(self, model, prompt, mcp_manager):
self.llm = model
self.prompt = prompt
self.mcp_manager = mcp_manager
self.tool_pattern = re.compile(r'\[([A-Za-z0-9\_]+\(.*?\),?\s?)+\]')
def _is_tool_required(self, response):
return bool(self.tool_pattern.match(response))
async def get_result_tool(self, response):
results = []
for name, params in self.parse_func_params(response):
is_error, content = await self.mcp_manager.call_tool(name, params)
results.append({"name": name, "output": [c.text for c in content]})
return json.dumps(results)
2. 對話流程控制
智能體的chat
方法實現完整的對話流程:
- 用戶提問:將用戶問題與工具調用說明組合為用戶提示。
- 模型響應:生成初步回答,判斷是否需要工具調用。
- 工具執行:若需要,調用MCP工具并獲取結果。
- 結果合成:將工具結果加入提示,生成最終回答。
async def chat(self, question):
tool_scheme = TOOL_CALL_PROMPT.format(function_scheme=self.mcp_manager.get_func_scheme())
user_msg = self.prompt.get_user_prompt(question, tool_scheme)
self.prompt.append_history(user_msg)
response = self.llm.generate(self.prompt.get_generation_prompt(tool_enabled=True))
if self._is_tool_required(response):
tool_result = await self.get_result_tool(response)
tool_msg = self.prompt.get_tool_result_prompt(tool_result)
self.prompt.append_history(tool_msg)
response = self.llm.generate(self.prompt.get_generation_prompt(tool_enabled=False))
return response
通過tool_enabled
參數的切換,智能體在工具調用決策階段包含工具指令,而在結果合成階段移除指令,避免模型過度關注工具調用,提升回答的連貫性。
四、實驗與優化:從問題發現到效果提升
(一)工具指令注入方式對比
1. 系統提示注入(持續暴露工具指令)
- 問題表現
合成回答時出現空響應或無關工具調用。
模型過度依賴工具,即使已有結果仍重復調用。
案例用戶查詢特定知識內容后,模型在回答時錯誤調用list_knowledges
工具。
2. 用戶提示注入(僅在需要時暴露)
- 優化策略
僅在生成工具調用決策時包含工具指令。
結果合成階段移除指令,專注于內容整合。
- 效果提升
回答相關性顯著提高,工具調用更精準。
模型能有效利用工具結果,如正確解析知識內容并生成摘要。
(二)實際應用案例
1. 知識摘要生成
- 查詢“請總結Obsidian知識庫中關于‘AI倫理’的筆記,并以Markdown表格呈現。”
- 流程
模型調用get_knowledge_by_uri
工具獲取筆記內容。
根據內容長度自動格式化為表格,盡管存在輕微參數錯誤,但結果結構化程度高。
2. 空筆記檢測
- 查詢“列出標題存在但內容為空的筆記。”
- 流程
模型調用list_knowledges
獲取所有筆記元數據。
通過文件字節大小判斷空筆記,結果部分正確,需進一步優化篩選邏輯。
3. 問題生成
- 查詢“根據‘機器學習基礎’筆記內容,生成5個簡答題。”
- 流程
調用工具獲取筆記內容。
模型分析內容結構,生成符合要求的問題,如“什么是監督學習?”
(三)性能與局限性
- 模型性能Llama 3.2 3B模型在本地CPU(MacBook M1)上的推理速度約為15 tokens/秒,適合交互式場景。
- 功能限制
多輪工具調用能力較弱,難以處理復雜推理任務。
對模糊查詢的理解不足,需明確參數(如正確的URI)才能有效執行工具。
代碼倉庫:https://github.com/hjlee94/mcp-knowledge-base