進階應用

多 Agent 工作流設計、串流輸出、效能調優與生產環境最佳實務。

Multi-agent 架構設計原則

當單一 Agent 面對複雜任務時,往往難以在一個 context window 內完成所有推理與行動。Multi-agent 架構透過職責分離與並行執行,解決這個限制。設計時應遵循以下原則:

設計 Multi-agent 系統時,先畫出資料流圖:哪些 Agent 產生資料、哪些 Agent 消費資料、哪些可以並行。可觀察性從設計階段就要考慮進去,而非事後補加。

Agent 評估與測試方法

Agent 的非確定性讓傳統單元測試難以直接套用,需要針對性的評估策略。

from langchain.evaluation import load_evaluator
from langchain_openai import ChatOpenAI

# --- 方法一:LLM-as-Judge(用另一個 LLM 評分)---
evaluator = load_evaluator(
    "criteria",
    llm=ChatOpenAI(model="gpt-4o"),
    criteria={
        "correctness": "答案是否事實正確?",
        "completeness": "答案是否完整回應問題?",
        "conciseness": "答案是否簡潔不冗餘?"
    }
)

result = evaluator.evaluate_strings(
    prediction="LangChain 是一個 LLM 應用框架,提供 Chain 和 Agent 等核心元件。",
    input="什麼是 LangChain?",
    reference="LangChain 是開源的 LLM 應用開發框架。"
)
print(result)  # {"score": 1, "reasoning": "..."}

# --- 方法二:RAG 評估(RAGAS 框架)---
# pip install ragas
from ragas import evaluate
from ragas.metrics import faithfulness, answer_relevancy, context_precision

dataset = {
    "question": ["LangChain 支援哪些向量資料庫?"],
    "answer": ["LangChain 支援 FAISS、Chroma、Pinecone 等。"],
    "contexts": [["LangChain 整合了超過 50 種向量資料庫..."]],
    "ground_truth": ["LangChain 支援 FAISS、Chroma、Pinecone、Weaviate 等。"]
}
score = evaluate(dataset, metrics=[faithfulness, answer_relevancy])
print(score)

# --- 方法三:確定性測試(針對工具呼叫路徑)---
def test_agent_calls_search_tool():
    """驗證 Agent 在需要查詢資料時確實呼叫了搜尋工具"""
    call_log = []
    original_search = search_tool.func

    def mock_search(query):
        call_log.append(query)
        return "模擬搜尋結果"

    search_tool.func = mock_search
    agent.invoke({"input": "搜尋最新的 AI 新聞"})
    assert len(call_log) > 0, "Agent 應該呼叫搜尋工具"
    search_tool.func = original_search
評估應覆蓋三個層次:工具呼叫正確性(是否選對工具)、答案品質(是否準確完整)、端對端任務完成率(是否達成使用者目標)。建議維護一個「黃金測試集」,每次版本更新都自動跑評估。

Memory 管理策略

記憶是 Agent 能夠跨對話保持上下文的關鍵機制,不同場景需要不同的記憶策略。

from langchain.memory import (
    ConversationBufferMemory,        # 短期:保留全部
    ConversationSummaryBufferMemory, # 摘要 + Buffer 混合策略
    ConversationTokenBufferMemory,   # 依 Token 數截斷
)
from langchain_openai import ChatOpenAI

llm = ChatOpenAI(model="gpt-4o-mini")

# 混合策略:近期保留原文,超過 token 限制的部分自動摘要
memory = ConversationSummaryBufferMemory(
    llm=llm,
    max_token_limit=2000,   # 超過 2000 token 就開始摘要
    return_messages=True
)

# 語意記憶:將使用者偏好持久化到向量資料庫
from langchain.memory import VectorStoreRetrieverMemory
from langchain_community.vectorstores import FAISS
from langchain_openai import OpenAIEmbeddings

vectorstore = FAISS.from_texts(["初始化"], OpenAIEmbeddings())
retriever = vectorstore.as_retriever(search_kwargs={"k": 3})
semantic_memory = VectorStoreRetrieverMemory(retriever=retriever)

# 儲存使用者偏好
semantic_memory.save_context(
    {"input": "我偏好簡潔的程式碼,不喜歡過多註解"},
    {"output": "已記住您的偏好"}
)

# 下次對話時,相關偏好會自動注入 Prompt
relevant = semantic_memory.load_memory_variables({"prompt": "幫我寫一段排序程式碼"})

Tool Design 原則

工具設計的品質直接影響 Agent 的可靠性。設計不良的工具是 Agent 失敗的最常見原因。

from langchain.tools import BaseTool
from pydantic import BaseModel, Field
from typing import Type
import json

# 定義輸入 Schema(Pydantic 自動驗證型別)
class SearchInput(BaseModel):
    query: str = Field(description="搜尋關鍵字,應為具體的問題或主題")
    max_results: int = Field(default=5, ge=1, le=20,
                             description="回傳結果數量,預設 5,最多 20")

class SearchTool(BaseTool):
    name: str = "web_search"
    description: str = (
        "搜尋網路上的最新資訊。"
        "適合用於查詢近期新聞、技術文件、特定主題的背景資料。"
        "不適合用於數學計算或固定事實查詢。"
    )
    args_schema: Type[BaseModel] = SearchInput

    def _run(self, query: str, max_results: int = 5) -> str:
        try:
            # 實際呼叫搜尋 API
            results = self._call_search_api(query, max_results)
            # 回傳結構化 JSON,方便 Agent 解析
            return json.dumps({
                "status": "success",
                "query": query,
                "results": results
            }, ensure_ascii=False)
        except Exception as e:
            # 錯誤訊息要對 LLM 有意義,讓它知道如何重試
            return json.dumps({
                "status": "error",
                "error_type": type(e).__name__,
                "message": str(e),
                "suggestion": "請嘗試使用更簡短的關鍵字重試"
            }, ensure_ascii=False)

    def _call_search_api(self, query, max_results):
        # 實際 API 呼叫實作
        return [{"title": "範例結果", "url": "https://example.com"}]

錯誤處理與重試機制

生產環境中,API 超時、速率限制、網路抖動是常態。健壯的 Agent 必須有完善的錯誤處理與重試策略。

from langchain_openai import ChatOpenAI
from langchain_core.runnables import RunnableConfig
from tenacity import retry, stop_after_attempt, wait_exponential, retry_if_exception_type
import openai

# --- 方法一:LangChain 內建重試 ---
llm = ChatOpenAI(model="gpt-4o-mini")
llm_with_retry = llm.with_retry(
    retry_if_exception_type=(
        openai.RateLimitError,
        openai.APITimeoutError,
        openai.APIConnectionError
    ),
    stop_after_attempt=3,
    wait_exponential_jitter=True  # 指數退避 + 隨機抖動,避免雷群效應
)

# --- 方法二:tenacity 自訂重試(更細粒度控制)---
@retry(
    stop=stop_after_attempt(3),
    wait=wait_exponential(multiplier=1, min=2, max=30),
    retry=retry_if_exception_type((openai.RateLimitError, TimeoutError)),
    reraise=True
)
def call_llm_with_retry(prompt: str) -> str:
    return llm.invoke(prompt).content

# --- 方法三:Fallback(降級到備用模型)---
from langchain_anthropic import ChatAnthropic

primary_llm = ChatOpenAI(model="gpt-4o")
fallback_llm = ChatAnthropic(model="claude-3-haiku-20240307")

# 主要模型失敗時自動切換到備用模型
llm_with_fallback = primary_llm.with_fallbacks([fallback_llm])

# --- 超時設定 ---
config = RunnableConfig(
    timeout=30,           # 整體 30 秒超時
    max_concurrency=5     # 最多 5 個並行請求
)
result = llm_with_fallback.invoke("你好", config=config)

成本控制策略

Token 費用在 Agent 應用中容易快速累積,尤其是 Multi-agent 架構或長對話場景。以下策略可有效控制成本:

成本監控的最佳實踐:在開發階段用 get_openai_callback() 追蹤每次呼叫的 Token 用量,找出最耗費 Token 的步驟,優先優化那些步驟。
from langchain_community.callbacks import get_openai_callback
from langchain_openai import ChatOpenAI
from langchain.globals import set_llm_cache
from langchain_community.cache import SQLiteCache

# 啟用 SQLite 快取(精確匹配快取,開發環境推薦)
set_llm_cache(SQLiteCache(database_path=".langchain_cache.db"))

llm = ChatOpenAI(model="gpt-4o-mini")

# 追蹤單次呼叫的 Token 消耗
with get_openai_callback() as cb:
    result = llm.invoke("解釋一下 RAG 的原理")
    print(f"輸入 Token:{cb.prompt_tokens}")
    print(f"輸出 Token:{cb.completion_tokens}")
    print(f"總費用(USD):${cb.total_cost:.6f}")

# 第二次相同呼叫直接從快取回傳,費用為 $0
result2 = llm.invoke("解釋一下 RAG 的原理")

生產環境部署注意事項

將 Agent 應用從開發環境遷移到生產環境時,有許多開發階段容易忽略的面向需要處理。

可觀測性與日誌設計

Agent 的非確定性讓傳統應用的監控方式不足以應對,需要專為 LLM 應用設計的可觀測性體系。

import logging
import json
import uuid
from datetime import datetime
from langchain.callbacks.base import BaseCallbackHandler

# 自訂 Callback Handler:記錄完整的 Agent 執行 trace
class StructuredLogHandler(BaseCallbackHandler):
    def __init__(self):
        self.trace_id = str(uuid.uuid4())
        self.logger = logging.getLogger("agent.trace")

    def on_llm_start(self, serialized, prompts, **kwargs):
        self.logger.info(json.dumps({
            "trace_id": self.trace_id,
            "event": "llm_start",
            "timestamp": datetime.utcnow().isoformat(),
            "model": serialized.get("name"),
            "prompt_length": sum(len(p) for p in prompts)
        }))

    def on_tool_start(self, serialized, input_str, **kwargs):
        self.logger.info(json.dumps({
            "trace_id": self.trace_id,
            "event": "tool_start",
            "timestamp": datetime.utcnow().isoformat(),
            "tool": serialized.get("name"),
            "input": input_str[:500]  # 截斷避免日誌過大
        }))

    def on_tool_end(self, output, **kwargs):
        self.logger.info(json.dumps({
            "trace_id": self.trace_id,
            "event": "tool_end",
            "timestamp": datetime.utcnow().isoformat(),
            "output_length": len(str(output))
        }))

    def on_agent_finish(self, finish, **kwargs):
        self.logger.info(json.dumps({
            "trace_id": self.trace_id,
            "event": "agent_finish",
            "timestamp": datetime.utcnow().isoformat(),
            "output": finish.return_values.get("output", "")[:500]
        }))

# 使用方式
from langchain.agents import AgentExecutor
handler = StructuredLogHandler()
executor = AgentExecutor(
    agent=agent,
    tools=tools,
    callbacks=[handler],  # 注入自訂 handler
    verbose=False          # 生產環境關閉 verbose
)
可觀測性工具推薦:LangSmith(LangChain 官方,最易整合)、Langfuse(開源,可自架)、Helicone(代理層方案,零程式碼整合)、OpenTelemetry(標準化分散式追蹤,適合已有 APM 基礎設施的團隊)。建議至少整合其中一個,否則生產環境除錯會非常困難。