前言
後端工程師的日常少不了讀文件——API 規格書、系統架構文件、RFC、技術白皮書、第三方服務的 migration guide… 這些文件動輒數十頁甚至上百頁,全部仔細讀完是不可能的,但只看標題又怕漏掉關鍵資訊。
我最近半年在公司內部建了一套「文件分析管線」,利用 LLM 來自動摘要、提取關鍵資訊、甚至回答特定問題。這篇文章會分享整套做法,從最基本的長文件切割策略,到 map-reduce 摘要模式,再到結構化輸出的實作技巧。
先說結論:AI 不能取代你讀文件,但可以幫你在 5 分鐘內掌握一份 50 頁文件的核心要點,知道哪些章節需要仔細看、哪些可以跳過。這對工作效率的提升是非常顯著的。
長文件切割策略
為什麼需要切割
目前主流 LLM 的 context window 雖然已經很大(Claude 200K tokens、GPT-4 128K tokens),但直接把整份文件塞進去有幾個問題:
- 成本:input token 越多越貴,100K tokens 一次呼叫就要好幾美元
- 注意力稀釋:研究表明 LLM 在超長 context 中,對中間段落的注意力會下降(”Lost in the Middle” 問題)
- 格式限制:有些文件是 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 '(未知)'}
要求:
- 開頭用 2-3 句話概述這份文件的目的和範圍
- 用章節式結構組織重點(不要只是把片段摘要串接起來)
- 合併重複的資訊
- 標註最重要的 3-5 個關鍵要點(用 粗體 標記)
- 如果有 breaking changes 或需要注意的事項,特別標出
- 結尾用一句話總結「讀完這份文件後,你應該知道什麼」
各片段摘要:
{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&A 系統
把上面的技巧組合起來,可以建一個簡單但實用的文件 Q&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 分析技術文件,關鍵心得:
- 切割策略很重要:好的切割方式能大幅提升摘要品質,優先用章節結構切割
- Map-Reduce 是處理長文件的標準模式:先分段摘要再合併,比直接塞整份文件效果好
- 結構化輸出讓資訊可操作:用 JSON schema 或 Pydantic 引導 AI 輸出結構化資料
- 快取很重要:同一份文件不需要每次都重新分析,hash + pickle 就能省大量 API 成本
- 設定好 system prompt 中的角色和技術棧:讓 AI 從你的角度分析,而不是泛泛而談
這套方法在我日常工作中最常用的場景:
- 評估第三方服務升級的影響
- 快速理解新同事交接的系統文件
- 整理會議紀錄和技術決策記錄
延伸閱讀建議:
- LangChain 的 Document Loaders — 支援各種文件格式
- Lost in the Middle 論文 — 理解 LLM 在長 context 下的行為
- Anthropic Prompt Engineering Guide — 進階 prompt 技巧