# 用 Qwen3 1.7B 打造你的 macOS 桌面自動化 Agent 完整指南 ![qwen3-macos-agent-cover](https://hackmd.io/_uploads/r1O8of9NWx.jpg) 想像一下,你對著電腦說「幫我整理下載資料夾,把所有 PDF 移到文件夾」,然後它就自動完成了。這不是科幻電影,而是你今天就能在自己的 Mac 上實現的事情。 本文將帶你從零開始,使用 Qwen3 1.7B 這個完全開源的小型語言模型,搭建一個能夠理解自然語言並執行實際操作的桌面自動化 Agent。 > [!tip] 為什麼選擇 Qwen3 1.7B? > - **完全本地運行**:不需要 API 金鑰,不需要網路連線 > - **隱私保護**:你的指令和資料永遠不會離開你的電腦 > - **原生 Function Calling**:專門為工具調用優化,準確率高 > - **資源友好**:僅需 ~1GB 記憶體,M1/M2 Mac 流暢運行 > - **Apache 2.0 授權**:完全開源,商用免費 --- ## 架構概覽 ![qwen3-agent-architecture](https://hackmd.io/_uploads/Sy443G94-e.jpg) 我們要搭建的 Agent 遵循標準的 ReAct(Reasoning + Acting)架構: ```mermaid flowchart LR A[用戶輸入] --> B[Qwen3 1.7B] B --> C{Function Calling} C --> D[檔案操作] C --> E[系統指令] C --> F[應用控制] C --> G[網頁操作] D --> H[執行結果] E --> H F --> H G --> H H --> B B --> I[回覆用戶] ``` --- ## 環境準備 ### 第一步:安裝 Ollama Ollama 是在 Mac 上運行本地 LLM 的最佳選擇,安裝過程非常簡單: ```bash # 方法一:使用 Homebrew(推薦) brew install ollama # 方法二:直接下載 # 前往 https://ollama.com/download 下載 macOS 版本 ``` 安裝完成後,啟動 Ollama 服務: ```bash # 啟動服務(會在背景運行) ollama serve ``` ### 第二步:下載 Qwen3 1.7B 模型 ```bash # 下載模型(約 1GB) ollama pull qwen3:1.7b # 驗證安裝 ollama list ``` > [!note] 模型變體選擇 > Ollama 提供多種 Qwen3 版本: > - `qwen3:0.6b` - 最輕量,適合資源受限環境 > - `qwen3:1.7b` - 平衡之選,Function Calling 表現優秀 > - `qwen3:4b` - 更強能力,需要更多記憶體 > - `qwen3:8b` - 最強能力,建議 16GB+ RAM ### 第三步:設置 Python 環境 ```bash # 建立虛擬環境 python3 -m venv agent-env source agent-env/bin/activate # 安裝必要套件 pip install ollama pyobjc pyautogui # 可選:安裝 Qwen-Agent 框架(功能更強大) pip install -U "qwen-agent[gui,rag,code_interpreter,mcp]" ``` --- ## 基礎實作:最小可行 Agent 讓我們從最簡單的版本開始,理解核心概念: ### 定義工具函數 ```python # agent_tools.py import os import subprocess from datetime import datetime def get_current_time() -> str: """取得目前時間""" return datetime.now().strftime("%Y-%m-%d %H:%M:%S") def list_files(directory: str) -> str: """列出指定目錄的檔案""" try: files = os.listdir(os.path.expanduser(directory)) return "\n".join(files[:20]) # 限制回傳數量 except Exception as e: return f"錯誤: {str(e)}" def move_file(source: str, destination: str) -> str: """移動檔案""" try: src = os.path.expanduser(source) dst = os.path.expanduser(destination) os.rename(src, dst) return f"成功將 {source} 移動到 {destination}" except Exception as e: return f"錯誤: {str(e)}" def run_shell_command(command: str) -> str: """執行 shell 指令(限制危險操作)""" # 安全檢查:禁止危險指令 dangerous = ['rm -rf', 'sudo', 'mkfs', 'dd if='] if any(d in command for d in dangerous): return "錯誤: 不允許執行危險指令" try: result = subprocess.run( command, shell=True, capture_output=True, text=True, timeout=30 ) return result.stdout or result.stderr or "指令執行完成" except subprocess.TimeoutExpired: return "錯誤: 指令執行超時" except Exception as e: return f"錯誤: {str(e)}" def open_application(app_name: str) -> str: """開啟 macOS 應用程式""" try: subprocess.run(['open', '-a', app_name], check=True) return f"已開啟 {app_name}" except Exception as e: return f"錯誤: {str(e)}" # 工具定義(供 LLM 使用) TOOLS = [ { "type": "function", "function": { "name": "get_current_time", "description": "取得目前的日期和時間", "parameters": {"type": "object", "properties": {}, "required": []} } }, { "type": "function", "function": { "name": "list_files", "description": "列出指定目錄中的檔案和資料夾", "parameters": { "type": "object", "properties": { "directory": { "type": "string", "description": "目錄路徑,例如 ~/Downloads 或 /Users/name/Documents" } }, "required": ["directory"] } } }, { "type": "function", "function": { "name": "move_file", "description": "將檔案從一個位置移動到另一個位置", "parameters": { "type": "object", "properties": { "source": {"type": "string", "description": "來源檔案路徑"}, "destination": {"type": "string", "description": "目標路徑"} }, "required": ["source", "destination"] } } }, { "type": "function", "function": { "name": "run_shell_command", "description": "執行 shell 指令,用於系統操作", "parameters": { "type": "object", "properties": { "command": {"type": "string", "description": "要執行的 shell 指令"} }, "required": ["command"] } } }, { "type": "function", "function": { "name": "open_application", "description": "開啟 macOS 應用程式", "parameters": { "type": "object", "properties": { "app_name": {"type": "string", "description": "應用程式名稱,如 Safari、Finder、Notes"} }, "required": ["app_name"] } } } ] # 函數映射 FUNCTION_MAP = { "get_current_time": get_current_time, "list_files": list_files, "move_file": move_file, "run_shell_command": run_shell_command, "open_application": open_application } ``` ### 建立 Agent 核心 ```python # agent_core.py import json from ollama import chat from agent_tools import TOOLS, FUNCTION_MAP class MacOSAgent: def __init__(self, model: str = "qwen3:1.7b"): self.model = model self.conversation_history = [] self.system_prompt = """你是一個 macOS 桌面自動化助手。 你可以幫助用戶執行檔案管理、開啟應用程式、執行系統指令等任務。 請使用繁體中文回覆。 重要規則: 1. 在執行任何操作前,先確認用戶的意圖 2. 對於可能影響系統的操作,要謹慎處理 3. 執行完成後,清楚回報結果 4. 如果遇到錯誤,提供解決建議""" def process_tool_calls(self, tool_calls): """處理工具調用並返回結果""" results = [] for tool_call in tool_calls: func_name = tool_call.function.name # 解析參數 try: args = json.loads(tool_call.function.arguments) except json.JSONDecodeError: args = {} # 執行函數 if func_name in FUNCTION_MAP: print(f" 🔧 執行工具: {func_name}") print(f" 參數: {args}") result = FUNCTION_MAP[func_name](**args) print(f" 結果: {result[:100]}..." if len(str(result)) > 100 else f" 結果: {result}") results.append({ "role": "tool", "content": result }) else: results.append({ "role": "tool", "content": f"未知工具: {func_name}" }) return results def chat(self, user_message: str) -> str: """與 Agent 對話""" # 添加用戶訊息 self.conversation_history.append({ "role": "user", "content": user_message }) # 準備完整訊息 messages = [ {"role": "system", "content": self.system_prompt} ] + self.conversation_history # 調用 LLM response = chat( model=self.model, messages=messages, tools=TOOLS ) assistant_message = response.message # 檢查是否有工具調用 if assistant_message.tool_calls: # 添加助手的工具調用訊息 self.conversation_history.append({ "role": "assistant", "content": assistant_message.content or "", "tool_calls": [ { "function": { "name": tc.function.name, "arguments": tc.function.arguments } } for tc in assistant_message.tool_calls ] }) # 執行工具並獲取結果 tool_results = self.process_tool_calls(assistant_message.tool_calls) self.conversation_history.extend(tool_results) # 讓 LLM 根據工具結果生成最終回覆 messages = [ {"role": "system", "content": self.system_prompt} ] + self.conversation_history final_response = chat( model=self.model, messages=messages, tools=TOOLS ) final_content = final_response.message.content self.conversation_history.append({ "role": "assistant", "content": final_content }) return final_content else: # 沒有工具調用,直接返回回覆 self.conversation_history.append({ "role": "assistant", "content": assistant_message.content }) return assistant_message.content def reset(self): """重置對話歷史""" self.conversation_history = [] def main(): """互動式 Agent 介面""" print("=" * 50) print("🤖 macOS 桌面自動化 Agent") print(" 模型: Qwen3 1.7B (本地運行)") print("=" * 50) print("輸入 'quit' 退出,'reset' 重置對話\n") agent = MacOSAgent() while True: try: user_input = input("👤 你: ").strip() if not user_input: continue if user_input.lower() == 'quit': print("再見!") break if user_input.lower() == 'reset': agent.reset() print("對話已重置。\n") continue print("\n🤖 Agent: ", end="") response = agent.chat(user_input) print(response) print() except KeyboardInterrupt: print("\n再見!") break if __name__ == "__main__": main() ``` --- ## 進階功能:macOS 專屬自動化 ### AppleScript 整合 macOS 的殺手鐧是 AppleScript,它能控制幾乎所有原生應用: ```python # applescript_tools.py import subprocess def run_applescript(script: str) -> str: """執行 AppleScript""" try: result = subprocess.run( ['osascript', '-e', script], capture_output=True, text=True, timeout=30 ) return result.stdout.strip() or result.stderr.strip() or "執行完成" except Exception as e: return f"錯誤: {str(e)}" def get_frontmost_app() -> str: """取得目前最前方的應用程式""" script = ''' tell application "System Events" return name of first application process whose frontmost is true end tell ''' return run_applescript(script) def set_volume(level: int) -> str: """設定系統音量 (0-100)""" level = max(0, min(100, level)) script = f'set volume output volume {level}' run_applescript(script) return f"音量已設為 {level}%" def send_notification(title: str, message: str) -> str: """發送系統通知""" script = f''' display notification "{message}" with title "{title}" ''' run_applescript(script) return "通知已發送" def get_clipboard() -> str: """取得剪貼簿內容""" script = 'return the clipboard' return run_applescript(script) def set_clipboard(content: str) -> str: """設定剪貼簿內容""" # 轉義特殊字元 content = content.replace('\\', '\\\\').replace('"', '\\"') script = f'set the clipboard to "{content}"' run_applescript(script) return "已複製到剪貼簿" def create_reminder(title: str, due_date: str = None) -> str: """在提醒事項中建立新提醒""" if due_date: script = f''' tell application "Reminders" make new reminder with properties {{name:"{title}", due date:date "{due_date}"}} end tell ''' else: script = f''' tell application "Reminders" make new reminder with properties {{name:"{title}"}} end tell ''' run_applescript(script) return f"提醒 '{title}' 已建立" def create_calendar_event(title: str, start_time: str, end_time: str) -> str: """在行事曆建立新事件""" script = f''' tell application "Calendar" tell calendar "行事曆" make new event with properties {{summary:"{title}", start date:date "{start_time}", end date:date "{end_time}"}} end tell end tell ''' run_applescript(script) return f"事件 '{title}' 已建立" def get_safari_url() -> str: """取得 Safari 目前頁面的 URL""" script = ''' tell application "Safari" return URL of current tab of front window end tell ''' return run_applescript(script) def open_url_in_safari(url: str) -> str: """在 Safari 開啟 URL""" script = f''' tell application "Safari" activate open location "{url}" end tell ''' run_applescript(script) return f"已在 Safari 開啟 {url}" ``` ### PyAutoGUI 視覺自動化 對於沒有 AppleScript 支援的應用程式,可以使用 PyAutoGUI: ```python # gui_automation.py import pyautogui import time # 安全設定 pyautogui.FAILSAFE = True # 移動到螢幕角落可以中止 pyautogui.PAUSE = 0.5 # 每個動作間隔 0.5 秒 def take_screenshot(filename: str = None) -> str: """擷取螢幕截圖""" if filename is None: filename = f"screenshot_{int(time.time())}.png" screenshot = pyautogui.screenshot() screenshot.save(filename) return f"截圖已保存: {filename}" def click_at(x: int, y: int) -> str: """在指定座標點擊""" pyautogui.click(x, y) return f"已點擊座標 ({x}, {y})" def type_text(text: str, interval: float = 0.05) -> str: """輸入文字""" pyautogui.typewrite(text, interval=interval) return f"已輸入: {text}" def hotkey(*keys) -> str: """按下組合鍵""" pyautogui.hotkey(*keys) return f"已按下組合鍵: {'+'.join(keys)}" def scroll_screen(clicks: int, x: int = None, y: int = None) -> str: """滾動螢幕""" pyautogui.scroll(clicks, x=x, y=y) direction = "上" if clicks > 0 else "下" return f"已向{direction}滾動 {abs(clicks)} 格" def get_mouse_position() -> str: """取得目前滑鼠位置""" x, y = pyautogui.position() return f"滑鼠位置: ({x}, {y})" def locate_on_screen(image_path: str) -> str: """在螢幕上尋找圖片位置""" try: location = pyautogui.locateOnScreen(image_path, confidence=0.9) if location: center = pyautogui.center(location) return f"找到圖片,中心位置: ({center.x}, {center.y})" return "未找到圖片" except Exception as e: return f"錯誤: {str(e)}" ``` --- ## 完整整合:Production-Ready Agent 將所有工具整合為一個完整的 Agent: ```python # macos_agent.py import json from ollama import chat from agent_tools import TOOLS as BASE_TOOLS, FUNCTION_MAP as BASE_FUNCTIONS from applescript_tools import ( run_applescript, get_frontmost_app, set_volume, send_notification, get_clipboard, set_clipboard, create_reminder, get_safari_url, open_url_in_safari ) from gui_automation import ( take_screenshot, click_at, type_text, hotkey, scroll_screen, get_mouse_position ) # 擴展工具定義 EXTENDED_TOOLS = BASE_TOOLS + [ { "type": "function", "function": { "name": "set_volume", "description": "設定系統音量", "parameters": { "type": "object", "properties": { "level": {"type": "integer", "description": "音量等級 (0-100)"} }, "required": ["level"] } } }, { "type": "function", "function": { "name": "send_notification", "description": "發送 macOS 系統通知", "parameters": { "type": "object", "properties": { "title": {"type": "string", "description": "通知標題"}, "message": {"type": "string", "description": "通知內容"} }, "required": ["title", "message"] } } }, { "type": "function", "function": { "name": "create_reminder", "description": "在提醒事項 App 建立新提醒", "parameters": { "type": "object", "properties": { "title": {"type": "string", "description": "提醒標題"}, "due_date": {"type": "string", "description": "到期日期,格式如 'January 15, 2025 10:00 AM'"} }, "required": ["title"] } } }, { "type": "function", "function": { "name": "take_screenshot", "description": "擷取螢幕截圖", "parameters": { "type": "object", "properties": { "filename": {"type": "string", "description": "儲存檔名(可選)"} }, "required": [] } } }, { "type": "function", "function": { "name": "get_clipboard", "description": "取得剪貼簿內容", "parameters": {"type": "object", "properties": {}, "required": []} } }, { "type": "function", "function": { "name": "set_clipboard", "description": "設定剪貼簿內容", "parameters": { "type": "object", "properties": { "content": {"type": "string", "description": "要複製的內容"} }, "required": ["content"] } } }, { "type": "function", "function": { "name": "open_url_in_safari", "description": "在 Safari 瀏覽器開啟網址", "parameters": { "type": "object", "properties": { "url": {"type": "string", "description": "要開啟的網址"} }, "required": ["url"] } } } ] # 擴展函數映射 EXTENDED_FUNCTIONS = { **BASE_FUNCTIONS, "set_volume": set_volume, "send_notification": send_notification, "create_reminder": create_reminder, "take_screenshot": take_screenshot, "get_clipboard": get_clipboard, "set_clipboard": set_clipboard, "open_url_in_safari": open_url_in_safari, } class ProductionAgent: """Production-ready macOS 自動化 Agent""" def __init__(self, model: str = "qwen3:1.7b"): self.model = model self.history = [] self.max_iterations = 5 # 防止無限循環 def run(self, task: str) -> str: """執行任務""" self.history = [{"role": "user", "content": task}] system = """你是一個專業的 macOS 桌面自動化助手。 你的能力包括: - 檔案和資料夾管理 - 系統設定調整 - 應用程式控制 - 提醒事項和行事曆管理 - 螢幕截圖 - 剪貼簿操作 - 網頁瀏覽 執行任務時: 1. 分析用戶需求,選擇合適的工具 2. 一步一步執行,確認每步結果 3. 遇到問題時提供解決方案 4. 完成後總結執行結果 請使用繁體中文回覆。""" for i in range(self.max_iterations): response = chat( model=self.model, messages=[{"role": "system", "content": system}] + self.history, tools=EXTENDED_TOOLS ) msg = response.message if msg.tool_calls: # 記錄工具調用 self.history.append({ "role": "assistant", "content": msg.content or "", "tool_calls": [ {"function": {"name": tc.function.name, "arguments": tc.function.arguments}} for tc in msg.tool_calls ] }) # 執行工具 for tc in msg.tool_calls: func_name = tc.function.name try: args = json.loads(tc.function.arguments) except: args = {} if func_name in EXTENDED_FUNCTIONS: result = EXTENDED_FUNCTIONS[func_name](**args) else: result = f"未知工具: {func_name}" self.history.append({"role": "tool", "content": str(result)}) else: # 沒有工具調用,任務完成 self.history.append({"role": "assistant", "content": msg.content}) return msg.content return "任務執行超過最大迭代次數" if __name__ == "__main__": agent = ProductionAgent() # 測試範例 tasks = [ "現在幾點了?", "幫我把音量調到 50%", "列出我下載資料夾裡的檔案", "發送一個通知提醒我開會" ] for task in tasks: print(f"\n📋 任務: {task}") print(f"🤖 回應: {agent.run(task)}") print("-" * 40) ``` --- ## 實用範例 ### 範例一:整理下載資料夾 ``` 👤: 幫我把下載資料夾裡的 PDF 檔案移到文件資料夾 🤖: 我來幫你整理。首先讓我看看下載資料夾裡有什麼... 🔧 執行工具: list_files 參數: {"directory": "~/Downloads"} 結果: report.pdf, image.png, video.mp4, document.pdf... 找到 2 個 PDF 檔案。現在移動它們... 🔧 執行工具: move_file 參數: {"source": "~/Downloads/report.pdf", "destination": "~/Documents/report.pdf"} 結果: 成功移動 🔧 執行工具: move_file 參數: {"source": "~/Downloads/document.pdf", "destination": "~/Documents/document.pdf"} 結果: 成功移動 完成!已將 2 個 PDF 檔案移動到文件資料夾。 ``` ### 範例二:建立提醒 ``` 👤: 提醒我明天下午三點開會 🤖: 🔧 執行工具: create_reminder 參數: {"title": "開會", "due_date": "January 7, 2025 3:00 PM"} 結果: 提醒 '開會' 已建立 已在提醒事項中建立「開會」提醒,時間設定為明天下午 3:00。 ``` ### 範例三:快速工作流 ``` 👤: 開啟 Safari 並幫我截圖 🤖: 🔧 執行工具: open_application 參數: {"app_name": "Safari"} 結果: 已開啟 Safari 🔧 執行工具: take_screenshot 參數: {"filename": "safari_screenshot.png"} 結果: 截圖已保存: safari_screenshot.png Safari 已開啟,螢幕截圖已保存為 safari_screenshot.png。 ``` --- ## 安全考量 > [!warning] 重要安全提醒 > 讓 AI 控制你的電腦是有風險的。請務必實作以下安全措施: ### 1. 指令白名單 ```python SAFE_COMMANDS = { 'ls', 'pwd', 'echo', 'cat', 'head', 'tail', 'mkdir', 'cp', 'mv', 'find', 'grep' } def is_safe_command(command: str) -> bool: """檢查指令是否安全""" first_word = command.split()[0] if command.split() else "" return first_word in SAFE_COMMANDS ``` ### 2. 確認機制 ```python def execute_with_confirmation(action: str, func, *args): """執行前要求用戶確認""" print(f"⚠️ 即將執行: {action}") confirm = input("確認執行?(y/n): ") if confirm.lower() == 'y': return func(*args) return "操作已取消" ``` ### 3. 沙箱目錄 ```python SANDBOX_DIR = os.path.expanduser("~/AgentSandbox") def validate_path(path: str) -> bool: """確保路徑在沙箱範圍內""" abs_path = os.path.abspath(os.path.expanduser(path)) return abs_path.startswith(SANDBOX_DIR) ``` --- ## 延伸閱讀 - [[深度研究_FunctionGemma使用場景與類似模型比較_20250106]] - Function Calling 模型比較 - [Ollama 官方文檔](https://ollama.com/library/qwen3) - [Qwen3 技術報告](https://qwenlm.github.io/blog/qwen3/) - [PyAutoGUI 文檔](https://pyautogui.readthedocs.io/) - [AppleScript 入門指南](https://developer.apple.com/library/archive/documentation/AppleScript/Conceptual/AppleScriptLangGuide/) --- ## 結語 使用 Qwen3 1.7B 搭建本地自動化 Agent 是一個完美的起點。它足夠小巧可以在任何現代 Mac 上流暢運行,同時具備出色的 Function Calling 能力。 從這個基礎出發,你可以: - 添加更多工具(郵件、訊息、音樂控制等) - 整合 MCP 協議連接更多服務 - 加入語音輸入實現真正的語音助手 - 建立自定義工作流自動化日常任務 本地 AI 的時代已經來臨,而你的 Mac 正是最佳的實驗場所。 --- *本文最後更新:2025-01-06*