Tutorial: MediGenius — LangGraph 多 Agent 醫療 AI 助手

一份「讀完就能理解架構 + 知道怎麼改 + 清楚資安邊界」的內部技術手冊。 目標讀者:已會 Python + FastAPI,想學 LangGraph multi-agent 編排、RAG pipeline、和 fallback chain 設計模式的工程師。


1. 為什麼要看 MediGenius?

1.1 「多 Agent」vs「純 Prompt」的關鍵差異

大多數 medical AI chatbot 的做法是「塞一大段 system prompt 進 LLM,讓它扮演醫生」——這是 pure-prompt approach(例如 Doctor 類 repo)。這做法簡單但有三個致命缺陷:

問題Pure-Prompt 做法MediGenius 做法
知識邊界LLM 只知道訓練資料裡的東西RAG 從醫學 PDF 即時檢索,知識可更新
正確性保障全靠 LLM 自己猜,無法驗證多層 fallback:RAG → LLM → Wikipedia → Tavily
可追蹤性回答沒有 source attribution每個回覆附 source 標籤(Medical Literature / Wikipedia / Web)
記憶每次對話獨立,無跨 session 記憶SQLite 持久化 + 20-turn sliding window
可擴充性加功能 = 改 prompt加功能 = 新增 Agent 節點 + routing 條件

1.2 這個 repo 適合學什麼

  • LangGraph StateGraph 實戰:8 個節點、6 條 conditional edge 的完整範例
  • RAG pipeline 從 PDF 到 ChromaDB:含 chunking 策略(512 chunk / 128 overlap)
  • Fallback chain 設計模式:planner → retriever → LLM → Wikipedia → Tavily → executor
  • FastAPI + React 全端整合:含 Docker、CI/CD、Render 部署
  • SQLite 長期記憶:SQLAlchemy ORM + session management

1.3 適用 / 不適用

適用不適用
學習 multi-agent 架構的教學範例生產環境醫療系統(缺 disclaimer、缺 HIPAA 合規)
RAG + fallback chain 設計參考中文醫療場景(keyword 全英文)
全端 AI app 快速起手高併發場景(in-memory state dict 無鎖)
Groq / LLaMA 3.3 整合範例需要 streaming response 的場景

2. 安裝與環境

2.1 前置條件

  • Python 3.10+
  • Node.js 18+
  • API Keys:
    • GROQ_API_KEY(從 Groq Console 取得)
    • TAVILY_API_KEY(從 Tavily AI 取得,選填——沒有則 Tavily fallback 不啟動)

2.2 本地安裝

 1# 1. Clone
 2git clone https://github.com/Md-Emon-Hasan/MediGenius.git
 3cd MediGenius
 4
 5# 2. 建立 .env(在 root 目錄)
 6cat > .env << 'EOF'
 7GROQ_API_KEY=your_groq_key_here
 8TAVILY_API_KEY=your_tavily_key_here
 9EOF
10
11# 3. 一鍵啟動(自動建 venv + 裝 backend/frontend deps)
12python run.py

啟動後:

  • Backend API:http://localhost:8000(Swagger docs:/docs
  • Frontend UI:http://localhost:5173

2.3 Docker 部署

1# 需要先在 root 目錄建 .env
2docker-compose up --build
  • Backend:port 8000
  • Frontend:port 80(Nginx reverse proxy)

2.4 依賴概覽

後端核心(backend/requirements.txt):

類別套件
APIFastAPI 0.128 + Uvicorn 0.40
AI/MLLangChain-core 1.2.6 + LangGraph 1.0.5 + LangChain-Groq 1.1.1
Embeddingsentence-transformers 5.2 + HuggingFace Hub 0.36
Vector DBChromaDB 1.4
PDFPyPDF 6.5
搜尋wikipedia 1.4 + duckduckgo-search 8.1
DBSQLAlchemy 2.0.45
推理PyTorch 2.9.1(sentence-transformers 需要)

前端:React 19 + Vite 7 + Tailwind CSS 4 + DaisyUI 5 + react-markdown


3. 架構深度解析

3.1 LangGraph StateGraph 總覽

MediGenius 的核心是一個 LangGraph StateGraph,定義在 backend/app/core/langgraph_workflow.py。整個 workflow 由 8 個 Agent 節點組成,透過 conditional edges 做 routing。


graph TD
    START((Start)) --> Memory["MemoryAgent
修剪歷史 ≤ 20 輪"] Memory --> Planner["PlannerAgent
Keyword Routing"] Planner -->|"含醫療關鍵字"| Retriever["RetrieverAgent
ChromaDB RAG"] Planner -->|"不含關鍵字"| LLM["LLMAgent
Direct LLM"] Retriever -->|"RAG 成功"| Executor["ExecutorAgent
合成最終回覆"] Retriever -->|"RAG 失敗"| LLM_fallback["LLMAgent
Fallback"] LLM -->|"LLM 成功"| Executor LLM -->|"LLM 失敗"| Retriever LLM_fallback -->|"LLM 成功"| Executor LLM_fallback -->|"LLM 失敗"| Wiki["WikipediaAgent
Wikipedia Search"] Wiki -->|"Wiki 成功"| Executor Wiki -->|"Wiki 失敗"| Tavily["TavilyAgent
Web Search"] Tavily --> Executor Executor --> END((End)) style Memory fill:#fdf6b2,stroke:#333 style Planner fill:#c9f,stroke:#333 style Retriever fill:#a0e3a0,stroke:#333 style LLM fill:#9fd4ff,stroke:#333 style LLM_fallback fill:#9fd4ff,stroke:#333 style Wiki fill:#ffe599,stroke:#333 style Tavily fill:#ffbdbd,stroke:#333 style Executor fill:#f9f,stroke:#333

3.2 AgentState — 共享狀態物件

所有節點共享一個 AgentState(TypedDict),定義在 backend/app/core/state.py

 1class AgentState(TypedDict):
 2    question: str                          # 使用者問題
 3    documents: List[Document]              # RAG 檢索到的文件
 4    generation: str                        # 最終回覆文字
 5    source: str                            # 來源標籤
 6    search_query: Optional[str]            # 搜尋查詢(預留)
 7    conversation_history: List[Dict]       # 對話歷史
 8    llm_attempted: bool                    # LLM 是否已嘗試
 9    llm_success: bool                      # LLM 是否成功
10    rag_attempted: bool                    # RAG 是否已嘗試
11    rag_success: bool                      # RAG 是否成功
12    wiki_attempted: bool                   # Wikipedia 是否已嘗試
13    wiki_success: bool                     # Wikipedia 是否成功
14    tavily_attempted: bool                 # Tavily 是否已嘗試
15    tavily_success: bool                   # Tavily 是否成功
16    current_tool: Optional[str]            # Planner 決定的工具
17    retry_count: int                       # 重試計數

設計重點:每個 Agent 只讀寫自己負責的 flag(如 rag_success),routing function 根據 flag 決定下一步。這是 LangGraph 的核心模式——狀態驅動的條件路由

3.3 各 Agent 節點詳解

Memory Agent(agents/memory.py)

最簡單的節點。做一件事:如果 conversation_history 超過 20 筆,截到最後 20 筆。

1def MemoryAgent(state: AgentState) -> AgentState:
2    history = state.get("conversation_history", [])
3    if len(history) > 20:
4        history = history[-20:]
5    state["conversation_history"] = history
6    return state

為什麼需要:防止下游 Agent 把太長的 history 塞進 LLM prompt 導致 token 爆掉。20 輪 = 40 messages(user + assistant),大約 4,000-8,000 tokens。

Planner Agent(agents/planner.py)

路由決策的核心。用 keyword matching 判斷使用者的問題是否含醫療關鍵字:

 1MEDICAL_KEYWORDS = [
 2    "fever", "pain", "headache", "nausea", "cancer", "diabetes",
 3    "treatment", "therapy", "medication", "heart", "lung", ...
 4]  # 共 ~130 個
 5
 6def PlannerAgent(state: AgentState) -> AgentState:
 7    question = state["question"].lower()
 8    contains_medical = any(kw in question for kw in MEDICAL_KEYWORDS)
 9    state["current_tool"] = "retriever" if contains_medical else "llm_agent"
10    return state

設計取捨

  • 優點:零延遲、零 cost(不需要呼叫 LLM 做 intent classification)
  • 缺點:(1) 新增醫療領域要手動加 keyword (2) 無法處理隱性醫療問題(如「我最近覺得很累」不含 keyword 但可能是醫療問題) (3) 只支援英文
  • 改進方向:可用 LLM 做 zero-shot classification,或用 embedding similarity 做 semantic routing

Retriever Agent(agents/retriever.py)

RAG 的核心。從 ChromaDB 向量資料庫檢索相關文件:

  1. 取最近 3 筆 user 訊息作為 context enrichment
  2. 組合成 combined_query = question + context
  3. 呼叫 retriever.invoke(combined_query)(ChromaDB cosine similarity,k=3)
  4. 過濾掉 < 50 字的無效文件
  5. 設定 rag_success flag

重要細節:Retriever 不做回答生成——它只檢索文件,把結果放進 state["documents"],交給 Executor 合成。

LLM Agent(agents/llm_agent.py)

直接用 Groq LLM 回答,不經過 RAG:

  1. 組裝最近 5 輪對話為 context
  2. System prompt:「You are a compassionate and knowledgeable medical AI assistant」
  3. 呼叫 Groq API(llama-3.3-70b-versatile,temperature=0.3)
  4. 回覆 > 10 字才算成功

溫度 0.3 的考量:醫療場景需要穩定、可重現的回答,不適合高溫度的創意生成。

Wikipedia Agent / Tavily Agent(fallback chain)

當 RAG 和 LLM 都失敗時的 fallback:

  • WikipediaWikipediaAPIWrapper,搜尋 question + "medical symptoms treatment",回傳 > 100 字才算成功
  • Tavily:web search API,搜尋 question + "medical health treatment symptoms",取 top 3 結果

Fallback 順序LLM → RAG → LLM → Wikipedia → Tavily → Executor(最終一定會到 Executor)

Executor Agent(agents/executor.py)

匯整所有上游結果,合成最終回覆:

  1. 如果有 documents(RAG / Wiki / Tavily 產出) → 取前 3 篇、每篇 1,000 字,用 LLM 合成回答
  2. 如果有 generation(LLM Agent 產出)→ 直接使用
  3. 都沒有 → 回退到預設回覆「Please consult a healthcare professional」
  4. 將 user + assistant 訊息 append 到 conversation_history

3.4 Routing Functions — 控制流的精髓

LangGraph 的 conditional edges 透過 routing functions 實作:

 1def _route_after_planner(state):
 2    return "retriever" if state["current_tool"] == "retriever" else "llm_agent"
 3
 4def _route_after_llm(state):
 5    return "executor" if state.get("llm_success") else "retriever"
 6
 7def _route_after_rag(state):
 8    return "executor" if state.get("rag_success") else "llm_agent"
 9
10def _route_after_wiki(state):
11    return "executor" if state.get("wiki_success") else "tavily"
12
13def _route_after_tavily(state):
14    return "executor"  # Tavily 是最後防線,無論如何都進 Executor

設計模式:每個 routing function 只看一個 boolean flag,非常簡潔。缺點是沒有 retry_count 的利用——理論上可以在重試 N 次後做不同決策。

3.5 ChatService — 編排層

backend/app/services/chat_service.py 是 workflow 的調用者:

  1. Startupinitialize_workflow() 編譯 LangGraph StateGraph
  2. Per-request
    • conversation_states dict 取或建 session state
    • reset_query_state() 清除前一次查詢的 flag(保留 conversation_history)
    • workflow_app.ainvoke(state) 執行整個 graph
    • 結果存入 SQLite + 更新 in-memory state

3.6 資料流全景

 1使用者輸入 "What are the symptoms of diabetes?"
 2    
 3    ├── POST /api/v1/chat
 4         └── ChatService.process_message()
 5               ├── db_service.save_message(user)
 6               ├── reset_query_state()
 7               ├── workflow.ainvoke(state)
 8                    ├── MemoryAgent: 修剪歷史
 9                    ├── PlannerAgent: "diabetes" 命中  current_tool="retriever"
10                    ├── RetrieverAgent: ChromaDB 找到 3 筆文件  rag_success=True
11                    └── ExecutorAgent:  LLM 合成文件內容為回覆
12               └── db_service.save_message(assistant)
13    
14    └── Response: { response: "...", source: "Medical Literature Database", ... }

4. RAG Pipeline 詳解

4.1 PDF 載入與切片

backend/app/tools/pdf_loader.py

 1# 1. 載入 PDF
 2loader = PyPDFLoader(pdf_path)  # 逐頁載入
 3docs = loader.load()
 4
 5# 2. 切片
 6splitter = RecursiveCharacterTextSplitter.from_tiktoken_encoder(
 7    chunk_size=512,      # 每個 chunk 512 tokens
 8    chunk_overlap=128,   # 重疊 128 tokens
 9    separators=["\n\n", ". ", "\n", " "],  # 優先在段落/句子邊界切割
10)
11splits = splitter.split_documents(docs)

為什麼 512/128

  • 512 tokens 大約 350-400 字,足以包含一個完整的醫學概念
  • 128 tokens 重疊確保跨 chunk 的上下文不會斷裂
  • from_tiktoken_encoder 確保 token 計數精確(vs 字元計數)

4.2 Embedding 與向量儲存

backend/app/tools/vector_store.py

  • Embedding modelsentence-transformers/all-MiniLM-L6-v2(384 維,輕量級)
  • Vector DB:ChromaDB,cosine similarity
  • Persist:存在 backend/storage/vector_store/
  • Retrievalas_retriever(search_kwargs={"k": 3})——每次取 top 3 最相似文件

啟動流程(在 main.py lifespan 中):

  1. 檢查 medical_book.pdf 是否存在
  2. process_pdf() 切片
  3. get_or_create_vectorstore(documents) — 如果已有 ChromaDB 檔就載入,否則建新的
  4. 只在首次啟動時做 embedding,之後直接載入 persist 目錄

4.3 自訂知識庫

要加入自己的醫學文件:

1# 方法 1:替換預設 PDF
2cp your_medical_book.pdf backend/data/medical_book.pdf
3rm -rf backend/storage/vector_store/*  # 清除舊向量
4# 重啟 backend,會自動重建
5
6# 方法 2:修改 config.py 指定多個 PDF(需改 code)
7# PDF_PATH 目前只支援單一檔案

5. 持久化與 Session 管理

5.1 SQLite 資料模型

backend/app/models/message.py

1class Message(Base):
2    __tablename__ = "messages"
3    id = Column(Integer, primary_key=True)
4    session_id = Column(String(255), index=True)  # UUID
5    role = Column(String(50))       # "user" | "assistant"
6    content = Column(Text)
7    source = Column(String(255))    # e.g. "Medical Literature Database"
8    timestamp = Column(DateTime, default=datetime.utcnow)

5.2 雙層記憶架構

MediGenius 有兩層記憶:

媒介生命週期用途
In-memory statePython dict(ChatService.conversation_statesprocess 存活期間提供 LangGraph workflow 的即時 context
Persistent storeSQLite永久跨 session 的歷史記錄、session 管理

重要限制:in-memory state 在 server 重啟後消失。雖然 SQLite 保存了完整歷史,但 conversation_history(workflow 用的那份)會重置。也就是說——重啟後第一次查詢不會有 conversation context。

5.3 Session API

EndpointMethod功能
/api/v1/chatPOST處理訊息,觸發 workflow
/api/v1/clearPOST清除 in-memory state
/api/v1/new-chatPOST建立新 session
/api/v1/historyGET取當前 session 歷史
/api/v1/sessionsGET列出所有 session
/api/v1/session/{id}GET載入特定 session
/api/v1/session/{id}DELETE刪除 session
/api/v1/healthGET健康檢查

6. 資安掃描

6.1 高風險項目

項目嚴重度說明
CORS 全開HIGHallow_origins=["*"] + allow_credentials=True — 任何網站都能發 credentialed request
Session secret 每次隨機MEDIUMsecret_key=secrets.token_hex(32) 在每次啟動時重新生成,重啟即失效所有 session
無 rate limitingMEDIUM沒有任何 API rate limit,可被暴力呼叫消耗 Groq API quota
無輸入驗證MEDIUMChatRequest.message 直接進 LLM prompt,無 sanitization
無 HTTPS 強制MEDIUMDocker 設定的 frontend 只開 port 80,production 需在 reverse proxy 層加 TLS

6.2 中風險項目

項目嚴重度說明
API key 管理MEDIUM-LOW用 .env + python-dotenv,符合基本實務;.gitignore 已排除 .env
SQLite 無加密LOW對話內容明文存儲,但這是 demo 專案可接受
無 authenticationMEDIUM任何人都能存取所有 API endpoint
醫療 disclaimer 缺失MEDIUMBenchmark 顯示 disclaimer rate = 0%,production 用途必須加上
PDF 上傳缺失LOW目前只支援預設 PDF,無動態上傳 endpoint(也因此無 file upload 攻擊面)

6.3 供應鏈依賴

  • PyTorch 2.9.1:大型依賴(~2 GB),僅用於 sentence-transformers;如果不需要本地 embedding 可考慮替換
  • 依賴版本固定requirements.txt== 鎖定版本,良好實務
  • requirements.txt lock file:pip freeze 產出的,沒有用 pip-tools / poetry.lock,但對 demo 專案可接受

6.4 Prompt Injection 風險

使用者輸入直接嵌入 LLM prompt(ExecutorAgent / LLMAgent),例如:

1prompt = (
2    "You are an experienced medical doctor...\n"
3    f"Patient's Current Question:\n{question}\n"  # 直接嵌入
4    f"Medical Information:\n{content}\n"
5)

攻擊者可透過精心設計的問題嘗試覆寫 system prompt。建議:加入 input sanitization + prompt 分離(system / user message)。


7. 前端架構

7.1 單檔式 React App

整個前端是一個 687 行的 App.jsx,包含 6 個 section:

  1. Imports:React hooks + ReactMarkdown
  2. Utility helpersformatTimeAgo() + buildDownloadText()
  3. Sidebar:session 列表、new chat、developer info、theme toggle
  4. ChatArea:welcome screen(含 6 個 quick questions)、messages、typing indicator
  5. InputArea:textarea + send button(支援 Enter 發送、Shift+Enter 換行)
  6. App root:所有 state + API logic(16 個 state variables)

7.2 UI 設計特色

  • Glassmorphism:大量使用 glass-effect class(半透明 + 模糊背景)
  • Dark/Light 切換:用 data-theme attribute + CSS variables
  • Responsive:768px 斷點,mobile 時 sidebar 變 overlay
  • Markdown 渲染:bot 回覆用 react-markdown 渲染,支援 code block / list / heading
  • Quick Questions:6 個預設問題按鈕,降低使用者首次互動門檻

7.3 API 串接

前端透過 /api/v1/* endpoint 與後端溝通。Vite dev server 和 Nginx production 都做 reverse proxy:

1// vite.config.js
2server: { proxy: { '/api': 'http://localhost:8000' } }
3
4// nginx.conf(production)
5location /api/ { proxy_pass http://backend:8000; }

8. 測試與 CI/CD

8.1 後端測試

12 個測試檔,約 1,084 行:

測試檔行數測試對象
test_agents.py204所有 8 個 Agent 的單元測試
test_workflow.py41LangGraph workflow 組裝
test_workflow_routing.py44Routing function 邏輯
test_api.py92API endpoint 整合測試
test_api_edge_cases.py47API 邊界條件
test_services.py154ChatService / DatabaseService
test_database.py74SQLite CRUD
test_tools.py159LLM client / vector store / search tools
test_coverage_gaps.py165補充覆蓋率缺口
test_logging.py36Logging 配置
test_app.py5App import 驗證
conftest.py63Pytest fixtures

執行方式:

1cd backend
2python -m pytest tests/                                    # 全部跑
3python -m pytest --cov=app tests/ --cov-report=term-missing  # 含覆蓋率

8.2 前端測試

1cd frontend
2npx vitest run    # 跑一次
3npm run test      # watch mode

8.3 CI/CD Pipeline

.github/workflows/ci-cd.yml 定義了三個 job:

1backend-test     →  flake8 + isort --check + pytest --cov
2frontend-test    →  npm install + vitest + npm build
3docker-build     →  (只在 push to master 時)docker build 兩個 image

9. 部署選項

9.1 本地開發

1python run.py   # 自動建 venv、裝 deps、啟動 backend + frontend

9.2 Docker Compose

1docker-compose up --build

注意事項:

  • docker-compose.yml 把 backend storage 掛載為 volume,確保 SQLite + ChromaDB 持久化
  • Frontend 用 multi-stage build(Node build → Nginx serve)
  • 沒有 .env 的 Docker-level 設定——需要在 docker-compose.ymlenvironment 裡加 API keys

9.3 Render(雲端)

render.yml 定義了 Render 的部署 manifest:

  • Backend:Python Web Service
  • Frontend:Static Site
  • Persistent disk:for SQLite storage

10. 與 Pure-Prompt 做法的對比分析

這一節比較 MediGenius 的 multi-agent 架構與典型 pure-prompt 醫療 chatbot 的差異。

10.1 架構對比

面向Pure-Prompt(如 Doctor repo)MediGenius
核心一個 LLM call + system prompt8 個 Agent 節點 + StateGraph
知識來源LLM 訓練資料PDF RAG + LLM + Wikipedia + Web search
路由決策無(所有問題走同一條路)Planner keyword routing + conditional fallback
記憶無或短期(conversation buffer)SQLite 持久化 + 20-turn sliding window
可追蹤性無 source 標示每個回覆附 source 標籤
錯誤處理LLM 幻覺就是最終答案多層 fallback:RAG → LLM → Wiki → Tavily
擴充性改 prompt新增 Agent 節點 + routing edge
Code 複雜度~50 行~2,000+ 行(backend)
延遲1 次 LLM call2-4 次工具呼叫 + 1 次 LLM call
成本1 次 API call2-5 次 API call

10.2 什麼時候該用 Pure-Prompt

  • Quick prototype:驗證概念,不需要正確性保障
  • 非醫療場景:幻覺風險可容忍的應用
  • Cost-sensitive:每次查詢只花一次 API call
  • Low-latency 需求:不能容忍多次工具呼叫的延遲

10.3 什麼時候該用 Multi-Agent

  • 需要 source attribution:使用者需要知道答案來自哪裡
  • 知識需要更新:PDF / 文件可以隨時替換,不用重新訓練 LLM
  • 需要 fallback:單一來源不可靠時有備案
  • 需要記憶:跨 session 的長期對話上下文
  • 需要審計:每個 Agent 的 flag 都記錄在 state 中,可追蹤決策路徑

10.4 MediGenius 的架構限制

  1. Planner 是 keyword 比對,不是語義理解:「我最近容易累」不會觸發 RAG
  2. 單一 PDF 限制:只支援一個 medical_book.pdf,沒有多文件管理
  3. Executor 是 terminal 節點:ExplanationAgent 被跳過(executor → END),缺少後處理
  4. No streaming:整個 workflow 是 sync 的(ainvoke 等全部完成才回覆)
  5. In-memory state dict 無鎖:多個 concurrent request 同一 session 可能 race condition
  6. LLM fallback 路徑有環:理論上 LLM → Retriever → LLM 可能循環(實務上 flag 機制會防止)

11. 改造建議與延伸方向

11.1 短期改進(Low-hanging fruit)

項目做法影響
加 rate limitingFastAPI middleware 用 slowapi防止 API abuse
收斂 CORS設定 allow_origins=["https://medigenius.onrender.com"]堵住 CSRF
加 disclaimer在 ExecutorAgent 回覆尾端固定附加免責聲明醫療合規
多 PDF 支援修改 process_pdf() 接受資料夾、glob 所有 PDF擴充知識庫
Streaming response用 FastAPI StreamingResponse + LangGraph streaming改善 UX

11.2 中期改進

項目做法
Semantic routing用 embedding similarity 取代 keyword matching
PubMed 整合新增 PubMedAgent,使用 Entrez API 搜尋學術文獻
多語言Planner 加入中文 keyword 或改用 language-agnostic classifier
Conversation context 持久化啟動時從 SQLite 恢復 conversation_history
分離 system / user prompt用 LangChain ChatPromptTemplate 的 system / human message 分離

11.3 長期方向

項目做法
接入 ExplanationAgent在 Executor 後加 post-processing(摘要、simplify、highlight)
引入 LLM-based Planner用小模型做 intent classification + tool selection
Human-in-the-loop不確定度高時要求使用者確認 / 重新提問
HIPAA 合規加密 SQLite、audit log、access control
微調 embedding用 medical corpus fine-tune sentence-transformers

附錄 A:快速啟動 Cheatsheet

 1# Clone + 設定
 2git clone https://github.com/Md-Emon-Hasan/MediGenius.git
 3cd MediGenius
 4echo "GROQ_API_KEY=gsk_xxx" > .env
 5echo "TAVILY_API_KEY=tvly_xxx" >> .env
 6
 7# 本地啟動
 8python run.py
 9
10# Docker 啟動
11docker-compose up --build
12
13# 跑後端測試
14cd backend && python -m pytest --cov=app tests/
15
16# 跑前端測試
17cd frontend && npx vitest run

附錄 B:關鍵檔案速查

檔案用途
backend/app/core/langgraph_workflow.pyLangGraph StateGraph 定義(8 節點 + 6 條 conditional edge)
backend/app/core/state.pyAgentState TypedDict + 初始化/重置函式
backend/app/agents/planner.py130 個醫療 keyword + routing 決策
backend/app/agents/retriever.pyChromaDB RAG 檢索
backend/app/agents/executor.py最終回覆合成
backend/app/tools/vector_store.pyChromaDB 建立/載入/retriever factory
backend/app/tools/pdf_loader.pyPDF 載入 + 切片
backend/app/services/chat_service.pyWorkflow 編排 + session state 管理
backend/app/services/database_service.pySQLite CRUD
backend/app/main.pyFastAPI app + lifespan(startup 流程)
frontend/src/App.jsxReact 全端 UI(687 行 monolithic)
docker-compose.ymlDocker 部署設定
.github/workflows/ci-cd.ymlCI/CD pipeline