2025年11月27日 星期四

AI - 本地 LLM Coding Agent 部署指南 (Win11 + Docker GitLab 雙帳號版)

 

本地 LLM Coding Agent 部署指南 (Win11 + Docker GitLab 雙帳號版)

本文件整合了在 Windows 11 筆電上建立「口語/文字驅動程式開發 Agent」的完整步驟。 此架構利用 NVIDIA 5090 的強大算力,結合 Ollama 與 Docker,實現從需求輸入到 GitLab 專案創建、程式碼提交、MR 建立的全自動化流程。

🗺️ 第一部分:總體步驟大綱

階段

步驟名稱

執行環境

執行頻率

目的

I

基礎設施準備

Windows (Admin)

僅需一次

安裝 Docker Desktop, Ollama, Git, .NET SDK

II

GitLab 與模型配置

Docker & Ollama

僅需一次

啟動本地 GitLab 容器,下載 DeepSeek Coder 模型

III

隔離工作區建置

各自帳號的 PowerShell

每個帳號各做一次

建立 Python Venv, 設定獨立的 .env (含 PAT)

IV

C# 專案初始化

各自帳號的 PowerShell

每個帳號各做一次

建立基礎 C# WinForm 專案結構

V

啟動服務與執行

PowerShell & WebUI

每次開發時

啟動 Agent API, Open WebUI, 執行自動化開發任務

📁 第二部分:資料夾結構圖

1. 系統共享資源 (C 槽根目錄)

此目錄存放 GitLab 容器的資料庫與儲存庫,所有使用者共用此服務。

C:\
└── gitlab_data\        <-- [共享] GitLab 容器的永續儲存空間
    ├── config\
    ├── data\
    └── logs\

2. 使用者隔離工作區 (各自的 User Home 目錄)

注意: 個人帳號與網域帳號需分別在其 Home 目錄下建立此結構。

C:\Users\<USERNAME>\
└── LLM_Coding_Agent\
    ├── .env                <-- [關鍵隔離] 存放該使用者的 GitLab PAT 和初始設定
    │
    ├── Agent_Scripts\      <-- [獨立] Python Agent 核心程式碼
    │   └── agent_api.py    <-- Flask API 啟動檔 (程式碼附於文件末尾)
    │
    ├── CSharp_Project\     <-- [獨立] C# 專案操作區
    │   ├── .git/
    │   └── WinFormApp/     <-- 實際的 C# 程式碼
    │
    └── venv\               <-- [獨立] Python 虛擬環境

💻 第三部分:詳細指令與驗證機制

階段 I:基礎設施準備 (系統共享)

執行身分: 具有管理員權限的 Windows 帳號。

步驟

環境

指令 / 操作

驗證機制 (Validation)

I.1

GUI

安裝 Docker Desktop

下載並安裝 Docker Desktop for Windows。

注意:安裝完成後請確保設定中開啟 WSL 2 backend。

PowerShell: docker --version

應顯示 Docker版本號。

I.2

GUI

安裝 Ollama (Desktop)

下載並安裝 Ollama Windows版。

PowerShell: ollama -v

應顯示 Ollama 版本號。

I.3

PowerShell

安裝開發工具

winget install Microsoft.Git -e

winget install Microsoft.DotNet.SDK.8 -e

PowerShell: git --version

dotnet --list-sdks

確認皆已安裝。

I.4

PowerShell

建立資料目錄

mkdir C:\gitlab_data

檔案總管: 確認 C 槽有 gitlab_data 資料夾。

階段 II:GitLab 與模型配置 (系統共享)

執行身分: 管理員或任何能執行 Docker 的帳號。

步驟

環境

指令 / 操作

驗證機制 (Validation)

II.1

PowerShell

啟動 GitLab 容器

docker run --detach --hostname localhost --publish 8080:80 --name gitlab --restart always --volume C:/gitlab_data/config:/etc/gitlab --volume C:/gitlab_data/logs:/var/log/gitlab --volume C:/gitlab_data/data:/var/opt/gitlab gitlab/gitlab-ce:latest

PowerShell: docker ps

應看到 gitlab 容器狀態為 Up

II.2

Browser

配置 GitLab (需等待 5-15分鐘啟動)

1. 開啟 http://localhost:8080

2. 設定 root 密碼並登入。

3. 關鍵: 進入 User Settings -> Access Tokens,生成一個 Personal Access Token (PAT),權限勾選 api

瀏覽器: 成功登入並取得 PAT 字串 (例如 glpat-xxxxxx)。

II.3

PowerShell

下載 LLM 模型

ollama run deepseek-coder:33b-instruct

PowerShell: ollama list

應看到 deepseek-coder 模型。

階段 III:隔離工作區建置 (各自執行)

執行身分: 分別在「個人帳號」與「網域帳號」登入後執行。

步驟

環境

指令 / 操作

驗證機制 (Validation)

III.1

PowerShell

建立目錄與虛擬環境

cd ~

mkdir LLM_Coding_Agent; cd LLM_Coding_Agent

mkdir Agent_Scripts CSharp_Project

python -m venv venv

.\venv\Scripts\Activate.ps1

PowerShell: 命令列前方出現 (venv)

III.2

(venv) PowerShell

安裝 Python 套件

pip install flask langchain langchain-ollama python-dotenv pyodbc requests

PowerShell: 無錯誤訊息,顯示安裝成功。

III.3

Editor

建立 .env 設定檔

LLM_Coding_Agent 根目錄建立 .env 檔案,內容如下:

GITLAB_URL=http://localhost:8080

GITLAB_PAT=你的_專屬_PAT_貼在這裡

SQL_SERVER=(localdb)\MSSQLLocalDB

手動檢查: 確認檔案存在,且 PAT 是該帳號專屬的。

III.4

Editor

部署 Agent 程式碼

本文末尾附錄的 Python 程式碼存檔為 Agent_Scripts\agent_api.py

手動檢查: 確認檔案已建立。

階段 IV:C# 專案初始化 (各自執行)

執行身分: 分別在兩個帳號下執行。

步驟

環境

指令 / 操作

驗證機制 (Validation)

IV.1

PowerShell

建立 C# 專案

cd LLM_Coding_Agent\CSharp_Project

dotnet new winforms -n WinFormApp

PowerShell: dir 應看到 WinFormApp 資料夾。

IV.2

PowerShell

Git 初始化

git init

git add .

git commit -m "Initial structure"

(註:不需要手動加 remote,Agent 會自動處理)

PowerShell: git log 應看到一筆 commit。

階段 V:啟動服務與執行 (日常操作)

執行身分: 當下要進行開發的帳號。

步驟

環境

指令 / 操作

驗證機制 (Validation)

V.1

PowerShell

啟動 Agent API (後端)

cd LLM_Coding_Agent

.\venv\Scripts\Activate.ps1

python Agent_Scripts\agent_api.py

PowerShell: 顯示 Running on http://127.0.0.1:5000

V.2

PowerShell

啟動 Open WebUI (前端)

docker run -d -p 3000:8080 --add-host=host.docker.internal:host-gateway -v open-webui:/app/backend/data --name open-webui --restart always ghcr.io/open-webui/open-webui:main

PowerShell: docker ps 顯示 open-webui 運作中。

V.3

Browser

WebUI 操作

1. 開啟 http://localhost:3000

2. 確保已設定 Tool Calling 指向 Flask API。

3. 輸入指令:

"我要開發一個計算機功能。請先在 GitLab 建立名為 Calculator_App 的私有專案。然後在 WinFormApp/Program.cs 加入註釋 '// Start',提交到 feat-calc 分支並建立 MR。"

GitLab (localhost:8080): 看到新專案 Calculator_App 被建立,且有一個新的 Merge Request。

PowerShell (V.1視窗): 看到 Agent 依序呼叫 create_gitlab_project -> write_file -> git 等工具的 Log。

🐍 附錄:Python Agent 核心程式碼 (agent_api.py)

請將以下程式碼複製並儲存至 Agent_Scripts\agent_api.py

import os
import subprocess
from flask import Flask, request, jsonify
from langchain.agents import AgentExecutor, create_tool_calling_agent
from langchain_core.prompts import ChatPromptTemplate
from langchain_ollama import Ollama
from langchain.tools import tool
from dotenv import load_dotenv
import pyodbc
import requests
import json

# 載入 .env 檔案
load_dotenv()

# --- 設定 ---
OLLAMA_BASE_URL = "http://localhost:11434"
MODEL_NAME = "deepseek-coder:33b-instruct" 
# 設定專案根目錄位置
PROJECT_ROOT = os.path.join(os.path.dirname(os.path.abspath(__file__)), '..', 'CSharp_Project')

# 從 .env 讀取設定
GITLAB_URL = os.getenv("GITLAB_URL")
GITLAB_PAT = os.getenv("GITLAB_PAT")
SQL_SERVER = os.getenv("SQL_SERVER")

# 初始化 Flask
app = Flask(__name__)

# 用來暫存當前操作的 GitLab Project ID
current_project_id = None

# --- 工具定義 (Tools) ---

@tool
def create_gitlab_project(name: str, description: str = "", visibility: str = "private") -> str:
    """
    使用 GitLab API 創建新專案。必須在開始新功能開發前調用此工具。
    成功後會自動將本地 Git remote 設定為新專案的 URL。
    name: 專案名稱 (如 'My-App')。
    """
    global current_project_id
    if not GITLAB_PAT or not GITLAB_URL:
        return "錯誤:缺少 GitLab PAT 或 URL。"

    url = f"{GITLAB_URL}/api/v4/projects"
    headers = {'Private-Token': GITLAB_PAT, 'Content-Type': 'application/json'}
    data = {'name': name, 'description': description, 'visibility': visibility, 'namespace_id': 1} # 預設 namespace 1 (root)
    
    try:
        response = requests.post(url, headers=headers, json=data)
        response.raise_for_status()
        project_info = response.json()
        current_project_id = str(project_info['id'])
        repo_url = project_info['http_url_to_repo']
        
        # 更新本地 Git Remote
        try:
            subprocess.run(['git', 'remote', 'set-url', 'origin', repo_url], cwd=PROJECT_ROOT, check=True, capture_output=True)
        except subprocess.CalledProcessError:
            try:
                subprocess.run(['git', 'remote', 'add', 'origin', repo_url], cwd=PROJECT_ROOT, check=True, capture_output=True)
            except:
                pass # 忽略錯誤,可能 remote 已存在
        
        return f"專案創建成功 ID: {current_project_id}, URL: {repo_url}"
    except Exception as e:
        return f"創建專案失敗: {e}"

@tool
def execute_local_git_command(command: str) -> str:
    """執行本地 Git 命令 (如 'add .', 'commit -m "msg"'). 不含 push。"""
    try:
        result = subprocess.run(['git'] + command.split(), cwd=PROJECT_ROOT, capture_output=True, text=True, check=True)
        return f"Git 執行成功: {result.stdout}"
    except subprocess.CalledProcessError as e:
        return f"Git 錯誤: {e.stderr}"

@tool
def push_to_gitlab_remote(branch_name: str) -> str:
    """執行 'git push origin <branch>' 推送至遠端。需先 Commit。"""
    try:
        result = subprocess.run(['git', 'push', 'origin', branch_name], cwd=PROJECT_ROOT, capture_output=True, text=True, check=True)
        return f"Push 成功: {result.stdout}"
    except subprocess.CalledProcessError as e:
        return f"Push 失敗: {e.stderr}"

@tool
def write_file_to_project(filepath: str, content: str) -> str:
    """在 C# 專案中寫入檔案。filepath 為相對路徑。"""
    full_path = os.path.join(PROJECT_ROOT, filepath)
    os.makedirs(os.path.dirname(full_path), exist_ok=True)
    try:
        with open(full_path, 'w', encoding='utf-8') as f:
            f.write(content)
        return f"寫入成功: {filepath}"
    except Exception as e:
        return f"寫入失敗: {e}"

@tool
def create_gitlab_merge_request(title: str, source_branch: str, target_branch: str = "main") -> str:
    """在 GitLab 創建 Merge Request。需在 Push 後調用。"""
    if not GITLAB_PAT or not current_project_id:
        return "錯誤:缺少 PAT 或尚未創建專案 (無 Project ID)。"

    url = f"{GITLAB_URL}/api/v4/projects/{current_project_id}/merge_requests"
    headers = {'Private-Token': GITLAB_PAT, 'Content-Type': 'application/json'}
    data = {'source_branch': source_branch, 'target_branch': target_branch, 'title': title}
    
    try:
        response = requests.post(url, headers=headers, json=data)
        response.raise_for_status()
        return f"MR 創建成功: {response.json()['web_url']}"
    except Exception as e:
        return f"MR 創建失敗: {e}"

@tool
def execute_sql_script(sql_query: str) -> str:
    """連接 SQL Server 執行查詢。"""
    if not SQL_SERVER: return "錯誤:未設定 SQL_SERVER。"
    conn_str = f"Driver={{ODBC Driver 17 for SQL Server}};Server={SQL_SERVER};Database=master;Trusted_Connection=yes;"
    try:
        conn = pyodbc.connect(conn_str)
        cursor = conn.cursor()
        cursor.execute(sql_query)
        if sql_query.strip().upper().startswith('SELECT'):
            rows = cursor.fetchall()
            conn.close()
            return f"查詢結果: {rows}"
        conn.commit()
        conn.close()
        return "SQL 執行成功。"
    except Exception as e:
        return f"SQL 錯誤: {e}"

# --- Agent 初始化 ---
def create_agent():
    llm = Ollama(model=MODEL_NAME, base_url=OLLAMA_BASE_URL)
    tools = [create_gitlab_project, execute_local_git_command, push_to_gitlab_remote, write_file_to_project, create_gitlab_merge_request, execute_sql_script]
    
    system_prompt = (
        "您是 C# 開發 Agent。請根據指令自主調用工具完成任務。"
        "若需要新專案,請務必先調用 create_gitlab_project。"
        "標準流程:建專案 -> 寫檔 -> Git Add/Commit -> Git Push -> 建立 MR。"
    )
    
    prompt = ChatPromptTemplate.from_messages([
        ("system", system_prompt),
        ("user", "{input}"),
        ("placeholder", "{agent_scratchpad}")
    ])
    
    agent = create_tool_calling_agent(llm, tools, prompt)
    return AgentExecutor(agent=agent, tools=tools, verbose=True)

agent_executor = create_agent()

# --- API Route ---
@app.route('/run_task', methods=['POST'])
def run_task():
    data = request.json
    user_input = data.get('prompt')
    if not user_input: return jsonify({"status": "error"}), 400
    try:
        response = agent_executor.invoke({"input": user_input})
        return jsonify({"status": "success", "result": response['output']})
    except Exception as e:
        return jsonify({"status": "error", "message": str(e)}), 500

if __name__ == '__main__':
    print(f"Agent 啟動於 C# 專案路徑: {PROJECT_ROOT}")
    app.run(host='127.0.0.1', port=5000)

沒有留言:

張貼留言