前言

後端工程師的日常少不了讀文件——API 規格書、系統架構文件、RFC、技術白皮書、第三方服務的 migration guide… 這些文件動輒數十頁甚至上百頁,全部仔細讀完是不可能的,但只看標題又怕漏掉關鍵資訊。

我最近半年在公司內部建了一套「文件分析管線」,利用 LLM 來自動摘要、提取關鍵資訊、甚至回答特定問題。這篇文章會分享整套做法,從最基本的長文件切割策略,到 map-reduce 摘要模式,再到結構化輸出的實作技巧。

先說結論:AI 不能取代你讀文件,但可以幫你在 5 分鐘內掌握一份 50 頁文件的核心要點,知道哪些章節需要仔細看、哪些可以跳過。這對工作效率的提升是非常顯著的。

長文件切割策略

為什麼需要切割

目前主流 LLM 的 context window 雖然已經很大(Claude 200K tokens、GPT-4 128K tokens),但直接把整份文件塞進去有幾個問題:

  1. 成本:input token 越多越貴,100K tokens 一次呼叫就要好幾美元
  2. 注意力稀釋:研究表明 LLM 在超長 context 中,對中間段落的注意力會下降(”Lost in the Middle” 問題)
  3. 格式限制:有些文件是 PDF、Word、HTML,需要先轉成純文字

切割方式比較

from typing import List
import re

# === 方法一:固定大小切割(最簡單但最粗暴)=== def split_by_size(text: str, chunk_size: int = 4000, overlap: int = 200) -> List[str]: """ 固定字數切割,帶重疊區域避免斷句。 優點:簡單快速 缺點:可能把段落切斷,破壞語意完整性 """ chunks = [] start = 0 while start < len(text): end = start + chunk_size # 嘗試在句號或換行處斷開 if end < len(text): # 往回找最近的段落結尾 break_point = text.rfind("\n\n", start, end) if break_point == -1: break_point = text.rfind("。", start, end) if break_point == -1: break_point = text.rfind(". ", start, end) if break_point > start: end = break_point + 1 chunks.append(text[start:end]) start = end - overlap # 重疊區域 return chunks

# === 方法二:按章節切割(推薦用於有結構的文件)=== def split_by_sections(text: str) -> List[dict]: """ 根據 Markdown 標題切割,保留章節結構。 回傳包含標題和內容的字典列表。 """ # 匹配 Markdown 標題(## 或 ###) pattern = r"^(#{1,3})\s+(.+)$" lines = text.split("\n")

sections = [] current_section = {"level": 0, "title": "Introduction", "content": []}

for line in lines: match = re.match(pattern, line) if match: # 儲存前一個 section if current_section["content"]: current_section["content"] = "\n".join(current_section["content"]) sections.append(current_section) current_section = { "level": len(match.group(1)), "title": match.group(2), "content": [], } else: current_section["content"].append(line)

# 別忘了最後一個 section if current_section["content"]: current_section["content"] = "\n".join(current_section["content"]) sections.append(current_section)

return sections

# === 方法三:語意切割(最精確但最慢)=== def split_by_semantics(text: str, max_chunk_size: int = 4000) -> List[str]: """ 利用 embedding 相似度判斷語意斷點。 相鄰段落的 embedding 相似度低於閾值時就切割。 """ import numpy as np from openai import OpenAI

client = OpenAI() paragraphs = [p.strip() for p in text.split("\n\n") if p.strip()]

# 取得每個段落的 embedding embeddings = [] for para in paragraphs: resp = client.embeddings.create( model="text-embedding-3-small", input=para[:8000], # 截斷太長的段落 ) embeddings.append(resp.data[0].embedding)

# 計算相鄰段落的相似度 similarities = [] for i in range(len(embeddings) - 1): sim = np.dot(embeddings[i], embeddings[i + 1]) similarities.append(sim)

# 在相似度低谷處切割 threshold = np.percentile(similarities, 25) # 取最低 25% 作為切割點

chunks = [] current_chunk = [paragraphs[0]] current_size = len(paragraphs[0])

for i in range(1, len(paragraphs)): if (current_size + len(paragraphs[i]) > max_chunk_size or (i - 1 < len(similarities) and similarities[i - 1] < threshold)): chunks.append("\n\n".join(current_chunk)) current_chunk = [paragraphs[i]] current_size = len(paragraphs[i]) else: current_chunk.append(paragraphs[i]) current_size += len(paragraphs[i])

if current_chunk: chunks.append("\n\n".join(current_chunk))

return chunks

我的選擇標準:

  • 有章節結構的文件(Markdown、有標題的文件)→ 方法二
  • 純文字、無結構(log、email 串)→ 方法一
  • 需要高精度切割(法律文件、合約)→ 方法三

Map-Reduce 摘要模式

當文件太長,單次 API 呼叫塞不下時,Map-Reduce 是最經典的處理模式:

Map 階段:逐 chunk 摘要

from anthropic import Anthropic

client = Anthropic()

def summarize_chunk(chunk: str, chunk_index: int, total_chunks: int) -> str: """對單一 chunk 做摘要(Map 階段)。""" response = client.messages.create( model="claude-sonnet-4-20250514", max_tokens=1024, messages=[{ "role": "user", "content": f"""請摘要以下技術文件片段的重點。這是第 {chunk_index}/{total_chunks} 個片段。

要求:

  • 保留所有技術細節(API 名稱、參數、版本號等)
  • 保留所有數字和量化資訊
  • 用條列式整理重點
  • 每個重點不超過兩句話
  • 如果片段中有程式碼範例,簡述其用途

文件片段: {chunk}""" }], temperature=0.1, ) return response.content[0].text

def map_phase(chunks: List[str]) -> List[str]: """Map 階段:平行處理所有 chunk。""" import concurrent.futures

summaries = [None] * len(chunks)

with concurrent.futures.ThreadPoolExecutor(max_workers=5) as executor: futures = { executor.submit(summarize_chunk, chunk, i + 1, len(chunks)): i for i, chunk in enumerate(chunks) } for future in concurrent.futures.as_completed(futures): idx = futures[future] summaries[idx] = future.result() print(f" Chunk {idx + 1}/{len(chunks)} 完成")

return summaries

Reduce 階段:合併摘要

def reduce_phase(summaries: List[str], original_title: str = "") -> str:
    """Reduce 階段:將所有 chunk 摘要合併為最終摘要。"""

combined = "\n\n---\n\n".join( f"【片段 {i+1} 摘要】\n{s}" for i, s in enumerate(summaries) )

response = client.messages.create( model="claude-sonnet-4-20250514", max_tokens=2048, messages=[{ "role": "user", "content": f"""以下是一份技術文件各片段的摘要。請將它們合併為一份完整、有組織的總摘要。

文件標題:{original_title or '(未知)'}

要求:

  1. 開頭用 2-3 句話概述這份文件的目的和範圍
  2. 用章節式結構組織重點(不要只是把片段摘要串接起來)
  3. 合併重複的資訊
  4. 標註最重要的 3-5 個關鍵要點(用 粗體 標記)
  5. 如果有 breaking changes 或需要注意的事項,特別標出
  6. 結尾用一句話總結「讀完這份文件後,你應該知道什麼」

各片段摘要: {combined}""" }], temperature=0.2, ) return response.content[0].text

完整流程串接

def summarize_document(text: str, title: str = "") -> str:
    """完整的文件摘要流程。"""
    print(f"文件長度: {len(text)} 字元")

# 1. 切割 chunks = split_by_sections(text) if not chunks: chunks = split_by_size(text) print(f"切割為 {len(chunks)} 個片段")

# 如果片段數 <= 3,直接合併處理(不需要 map-reduce) if len(chunks) <= 3: all_text = "\n\n".join( c["content"] if isinstance(c, dict) else c for c in chunks ) if len(all_text) < 50000: return single_pass_summary(all_text, title)

# 2. Map chunk_texts = [ c["content"] if isinstance(c, dict) else c for c in chunks ] print("Map 階段開始...") summaries = map_phase(chunk_texts)

# 3. Reduce print("Reduce 階段...") final_summary = reduce_phase(summaries, title)

return final_summary

結構化輸出

很多時候我們不只需要摘要,還需要從文件中提取特定的結構化資訊。

用 Pydantic 定義輸出格式

from pydantic import BaseModel, Field
from typing import Optional
import json

class APIEndpoint(BaseModel): method: str = Field(description="HTTP method (GET/POST/PUT/DELETE)") path: str = Field(description="API path") description: str = Field(description="功能描述") breaking_change: bool = Field(default=False, description="是否為 breaking change")

class MigrationStep(BaseModel): order: int = Field(description="步驟順序") action: str = Field(description="需要執行的操作") command: Optional[str] = Field(default=None, description="相關的指令或程式碼") rollback: Optional[str] = Field(default=None, description="回滾方式")

class DocumentAnalysis(BaseModel): title: str version: Optional[str] = None summary: str = Field(description="3-5 句話的摘要") key_changes: list[str] = Field(description="關鍵變更列表") api_endpoints: list[APIEndpoint] = Field(default_factory=list) migration_steps: list[MigrationStep] = Field(default_factory=list) breaking_changes: list[str] = Field(default_factory=list) action_items: list[str] = Field(description="讀完後需要做的事情")

def extract_structured_info(text: str) -> DocumentAnalysis: """從文件中提取結構化資訊。""" schema = DocumentAnalysis.model_json_schema()

response = client.messages.create( model="claude-sonnet-4-20250514", max_tokens=4096, messages=[{ "role": "user", "content": f"""請分析以下技術文件,並按照指定的 JSON schema 提取結構化資訊。

JSON Schema:

json
{json.dumps(schema, indent=2, ensure_ascii=False)}

請直接回覆符合此 schema 的 JSON,不要加其他文字。

文件內容: {text[:50000]}""" }], temperature=0.1, )

content = response.content[0].text.strip() content = re.sub(r"^

json\s*”, “”, content)
content = re.sub(r”\s*“$", "", content)

data = json.loads(content)
return DocumentAnalysis(**data)
<pre><code>### 實際應用:分析 Release Notes</code></pre>python
def analyze_release_notes(release_notes_text: str):
"""分析第三方服務的 release notes,找出影響我們的變更。"""

response = client.messages.create(
model="claude-sonnet-4-20250514",
max_tokens=4096,
system="""你是一位後端工程師,正在閱讀第三方服務的 release notes。
你的團隊使用以下技術棧:Python, FastAPI, PostgreSQL, Redis, Docker, Kubernetes。
請從後端工程師的角度分析這份 release notes。""",
messages=[{
"role": "user",
"content": f"""請分析以下 release notes,回覆 JSON 格式:

{{
"impact_level": "high|medium|low|none",
"breaking_changes": ["列出所有 breaking changes"],
"relevant_features": ["與我們技術棧相關的新功能"],
"deprecations": ["即將棄用的功能"],
"action_items": ["我們需要採取的行動"],
"timeline": "建議的升級時程"
}}

Release Notes:
{release_notes_text}"""
}],
temperature=0.1,
)

return json.loads(response.content[0].text)
<pre><code>## 實際應用案例:技術文件 Q&amp;A 系統

把上面的技巧組合起來,可以建一個簡單但實用的文件 Q&amp;A 系統:</code></pre>python
import hashlib
import pickle
from pathlib import Path

class DocumentQA:
"""技術文件 Q&A 系統。支援載入多份文件並回答問題。"""

def __init__(self, cache_dir: str = ".doc_cache"):
self.client = Anthropic()
self.cache_dir = Path(cache_dir)
self.cache_dir.mkdir(exist_ok=True)
self.documents = {}

def load_document(self, filepath: str, title: str = ""):
"""載入文件並建立摘要快取。"""
text = Path(filepath).read_text(encoding="utf-8")
doc_hash = hashlib.md5(text.encode()).hexdigest()
cache_file = self.cache_dir / f"{doc_hash}.pkl"

if cache_file.exists():
print(f"從快取載入: {filepath}")
doc_data = pickle.loads(cache_file.read_bytes())
else:
print(f"分析文件中: {filepath}")
chunks = split_by_sections(text)
chunk_texts = [
c["content"] if isinstance(c, dict) else c for c in chunks
]
summaries = map_phase(chunk_texts)
overall = reduce_phase(summaries, title)

doc_data = {
"title": title or Path(filepath).name,
"chunks": chunk_texts,
"summaries": summaries,
"overall_summary": overall,
"full_text": text,
}
cache_file.write_bytes(pickle.dumps(doc_data))

self.documents[doc_data["title"]] = doc_data
print(f"已載入: {doc_data['title']} ({len(doc_data['chunks'])} 個片段)")

def ask(self, question: str) -> str:
"""根據已載入的文件回答問題。"""
# 組合所有文件的摘要作為 context
context_parts = []
for title, doc in self.documents.items():
context_parts.append(f"## {title}\n{doc['overall_summary']}")

context = "\n\n---\n\n".join(context_parts)

response = self.client.messages.create(
model="claude-sonnet-4-20250514",
max_tokens=2048,
system="""你是一個技術文件助手。根據提供的文件摘要回答問題。
如果文件中沒有相關資訊,請明確說明。不要猜測。
回答時引用具體的文件名稱和章節。""",
messages=[{
"role": "user",
"content": f"""已載入的文件摘要:

{context}

問題:{question}"""
}],
temperature=0.2,
)
return response.content[0].text

# === 使用範例 ===
if __name__ == "__main__":
qa = DocumentQA()

# 載入多份文件
qa.load_document("docs/api-v3-migration.md", "API v3 Migration Guide")
qa.load_document("docs/postgresql-16-release.md", "PostgreSQL 16 Release Notes")
qa.load_document("docs/k8s-1.29-changelog.md", "Kubernetes 1.29 Changelog")

# 提問
answer = qa.ask("PostgreSQL 16 有哪些效能改進?我們升級時需要注意什麼?")
print(answer)

answer = qa.ask("API v3 有哪些 breaking changes?migration 的建議步驟是什麼?")
print(answer)

小結

用 AI 分析技術文件,關鍵心得:

  1. 切割策略很重要:好的切割方式能大幅提升摘要品質,優先用章節結構切割
  2. Map-Reduce 是處理長文件的標準模式:先分段摘要再合併,比直接塞整份文件效果好
  3. 結構化輸出讓資訊可操作:用 JSON schema 或 Pydantic 引導 AI 輸出結構化資料
  4. 快取很重要:同一份文件不需要每次都重新分析,hash + pickle 就能省大量 API 成本
  5. 設定好 system prompt 中的角色和技術棧:讓 AI 從你的角度分析,而不是泛泛而談

這套方法在我日常工作中最常用的場景:

  • 評估第三方服務升級的影響
  • 快速理解新同事交接的系統文件
  • 整理會議紀錄和技術決策記錄

延伸閱讀建議: