AI PRO·Harness Day 4 Context层设计

作者:


系列导航

第一篇:什么是Harness Engineering

第二篇:六层架构详解

第三篇:Prompt层设计

第四篇:Context层设计 ← 你在这里

第五篇:Tool层设计

第六篇:Memory层设计

第七篇:Eval层设计

第八篇:Observability层设计


一、引言:上下文是Agent的”工作记忆”

想象你是一位外科医生,正在做一台复杂的手术。你不可能把所有的医学教科书都背在脑子里,但你需要在手术过程中随时调取相关知识——病人的病历、影像资料、手术方案、药物禁忌……这些即时可及的知识,就是你的”工作记忆”。

对于AI Agent来说,Context(上下文)就是它的工作记忆

一个没有良好Context管理的Agent,就像一个失忆的外科医生——每次对话都要从头开始,对用户的历史一无所知,对专业知识只能靠”猜”。而一个Context层设计精良的Agent,则像一位准备充分的专家:它知道你是谁,了解你的历史需求,掌握相关领域的专业知识,并且能够精准地在需要的时候把正确的信息放到正确的位置。

1.1 为什么Context层如此关键?

在Harness Engineering的六层架构中,Context层位于Prompt层Tool层之上,是连接”模型能力”与”知识供给”的桥梁。

`

┌─────────────────────────────────────────────┐

│ Orchestration 编排层 │

│ ┌─────────────────────────┐ │

│ │ Context 上下文层 │ │ ← 本篇主题

│ │ ┌──────────┐ ┌────────┐│ │

│ │ │Prompt层 │ │Tool层 ││ │

│ │ └──────────┘ └────────┘│ │

│ └─────────────────────────┘ │

│ ┌─────────────────────────┐ │

│ │ Memory 记忆层 │ │

│ └─────────────────────────┘ │

│ ┌─────────────────────────┐ │

│ │ Model 模型层 │ │

│ └─────────────────────────┘ │

└─────────────────────────────────────────────┘

`

类比:如果Prompt层是”指令”,Tool层是”工具”,那么Context层就是”材料”。就像做菜一样——你有菜谱(Prompt),有厨具(Tool),但没有食材(Context),什么也做不出来。

1.2 Context层的核心职责

职责 描述 示例

|——|——|——|

知识供给 为模型提供回答问题所需的知识 文档检索、知识库查询
历史管理 维护对话历史和用户状态 多轮对话上下文
信息筛选 从海量数据中提取相关信息 RAG检索、重排序
窗口管理 在有限窗口内最大化信息密度 上下文压缩、滑动窗口
动态注入 根据任务需求动态组装上下文 任务感知的上下文选择

二、上下文窗口的本质

2.1 Token限制与信息密度

所有大语言模型都有一个上下文窗口(Context Window)——它是模型在单次推理中能够”看到”的最大文本长度。

类比:上下文窗口就像你的桌面面积。桌面越大,你能同时摊开的文件越多。但桌面再大也有边界——你不可能把整个图书馆摊在一张桌子上。

模型 上下文窗口 大约等于

|——|———–|———|

GPT-4o 128K tokens ~300页A4文档
Claude 3.5 Sonnet 200K tokens ~500页A4文档
Gemini 1.5 Pro 1M tokens ~2500页A4文档
GPT-4o mini 128K tokens ~300页A4文档
DeepSeek-V3 128K tokens ~300页A4文档
Llama 3.1 128K tokens ~300页A4文档

窗口大小 ≠ 有效信息量。这里有两个关键概念:

① 信息密度(Information Density)

`

信息密度 = 有效信息量 / Token数量

`

一段精心组织的技术文档,信息密度可能达到0.8;而一段冗余重复的对话,信息密度可能只有0.1。

② 注意力衰减(Attention Decay)

即使在窗口范围内,模型对不同位置信息的关注程度也不均匀:

`

注意力分布示意图(非线性):

注意力强度

高 │ ██ ██

│ ████ ████

│ ██████ ██████

│ ████████ ████████

低 │ ████████████████████████████████████████

└──────────────────────────────────────────→

开头(指令) 中间(上下文) 末尾(最近对话)

`

这就是著名的 “Lost in the Middle” 现象——模型对上下文开头和结尾的信息注意力最强,对中间部分的信息容易忽略。

2.2 上下文的三个层次

一个完整的Agent上下文通常由三个层次组成:

`python

context = {

“system”: “…”, # 系统指令层:角色定义、行为规范

“retrieved”: “…”, # 检索知识层:从知识库中检索的文档

“conversation”: “…” # 对话历史层:用户与Agent的交互记录

}

`

层次 特征 生命周期 典型占比

|——|——|———|———|

System(系统层) 相对固定,定义Agent身份和行为 整个会话期间 5-15%
Retrieved(检索层) 动态变化,根据用户问题检索 单次推理 30-60%
Conversation(对话层) 随对话累积增长 滑动窗口 25-50%

类比:这就像一个人的工作状态——System是你的职业身份(”我是医生”),Retrieved是你查阅的资料(”病人的CT报告”),Conversation是你和病人的对话记录(”上次你说头痛对吗?”)。


三、Context管理的三大策略

面对上下文窗口有限、知识需求无限的矛盾,工程界发展出了三大策略:

`

┌─────────────────────────────────────────────────────┐

│ Context管理策略 │

├─────────────┬─────────────┬─────────────────────────┤

│ RAG │ 长上下文 │ 动态注入 │

│ 检索增强 │ 压缩扩展 │ 智能组装 │

├─────────────┼─────────────┼─────────────────────────┤

│ 从外部知识库 │ 利用超长上下 │ 根据任务类型 │

│ 检索相关文档 │ 文窗口塞入 │ 动态选择和组装 │

│ 注入上下文 │ 更多信息 │ 最优上下文 │

├─────────────┼─────────────┼─────────────────────────┤

│ 适合:知识库 │ 适合:长文档 │ 适合:复杂任务 │

│ 问答、客服 │ 分析、总结 │ 多步骤推理 │

└─────────────┴─────────────┴─────────────────────────┘

`

在实际应用中,这三种策略往往混合使用。接下来我们逐一深入。


四、RAG:检索增强生成

4.1 RAG架构设计

RAG(Retrieval-Augmented Generation)是目前最主流的上下文管理方案。它的核心思想是:不把所有知识都塞进上下文,而是在推理时根据用户问题,从外部知识库中检索最相关的文档片段,然后注入上下文

类比:RAG就像一个经验丰富的律师——他不会把所有法律条文都背下来,但他知道如何快速检索到相关的法条,然后结合这些法条来回答你的问题。

标准RAG架构如下:

`

用户问题

┌──────────────┐ ┌──────────────────┐

│ Query处理 │────▶│ 向量检索 │

│ (Embedding) │ │ (Vector Search) │

└──────────────┘ └────────┬─────────┘

┌─────────▼─────────┐

│ 重排序 │

│ (Reranking) │

└─────────┬─────────┘

┌──────────────┐ ┌────────▼──────────┐

│ LLM推理 │◀────│ 上下文组装 │

│ 生成回答 │ │ (Context Build) │

└──────────────┘ └───────────────────┘

┌─────────┴─────────┐

│ 向量数据库 │

│ (Vector Store) │

└───────────────────┘

┌─────────┴─────────┐

│ 文档分块 │

│ (Chunking) │

└───────────────────┘

原始文档

`

4.2 文档分块策略

文档分块是RAG的第一步,也是最影响最终效果的一步。分块的质量直接决定了检索的质量。

类比:分块就像图书馆的编目方式。如果把整本书当成一个条目,你很难找到具体的段落;如果把每个字当成一个条目,又失去了上下文。好的分块就像好的编目——既精确又完整。

4.2.1 固定大小分块(Fixed-size Chunking)

最简单的方式,按固定Token数量切分:

`python

def fixed_size_chunking(text: str, chunk_size: int = 512, overlap: int = 64) -> list[str]:

“””固定大小分块,带重叠”””

tokens = tokenize(text)

chunks = []

start = 0

while start < len(tokens):

end = start + chunk_size

chunk = detokenize(tokens[start:end])

chunks.append(chunk)

start = end – overlap # 重叠部分保证上下文连贯

return chunks

`

优点:实现简单,分块大小可预测

缺点:可能在句子中间切断,破坏语义完整性

4.2.2 语义分块(Semantic Chunking)

根据语义边界切分,比如按段落、章节、或语义相似度:

`python

from langchain.text_splitter import RecursiveCharacterTextSplitter

splitter = RecursiveCharacterTextSplitter(

chunk_size=512,

chunk_overlap=64,

separators=[“nn”, “n”, “。”, “!”, “?”, “;”, ” “],

length_function=len,

)

chunks = splitter.split_text(document)

`

4.2.3 递归分块(Recursive Chunking)

LangChain推荐的默认策略,按层级分隔符递归切分:

`python

# 优先按段落切分 → 段落太长则按句子切分 → 句子太长则按词切分

separators = [“nn”, “n”, “。”, “.”, ” “, “”]

`

分块策略对比

策略 复杂度 语义完整性 适用场景

|——|——–|———–|———|

固定大小 ⭐⭐ 结构化程度低的文档
语义分块 ⭐⭐⭐ ⭐⭐⭐⭐ 文章、报告等有明确段落的文档
递归分块 ⭐⭐ ⭐⭐⭐ 通用场景(推荐默认选择)
按文档结构 ⭐⭐⭐⭐ ⭐⭐⭐⭐⭐ 技术文档、Wiki等结构化内容

实践建议:chunk_size建议设置为256-1024 tokens,overlap建议为chunk_size的10%-15%。

4.3 Embedding模型选择

Embedding模型将文本转换为向量表示,是语义检索的基础。选择合适的Embedding模型对检索质量至关重要。

类比:Embedding就像给每段文本分配一个”GPS坐标”。意思相近的文本,它们的”坐标”就接近;意思不同的文本,”坐标”就远离。

模型 维度 中文支持 价格 推荐场景

|——|——|———|——|———|

text-embedding-3-small 1536 ⭐⭐⭐ $0.02/M tokens 通用场景,性价比高
text-embedding-3-large 3072 ⭐⭐⭐ $0.13/M tokens 需要高精度的场景
embed-v4 (Cohere) 1024 ⭐⭐⭐⭐ $0.1/M tokens 多语言场景
BGE-M3 (BAAI) 1024 ⭐⭐⭐⭐⭐ 免费(自部署) 中文场景首选
bge-large-zh-v1.5 1024 ⭐⭐⭐⭐⭐ 免费(自部署) 纯中文场景
Jina-embeddings-v3 1024 ⭐⭐⭐⭐ 免费额度 多语言、长文本

`python

# OpenAI Embedding示例

from openai import OpenAI

client = OpenAI()

def get_embedding(text: str, model: str = “text-embedding-3-small”) -> list[float]:

response = client.embeddings.create(

input=text,

model=model

)

return response.data[0].embedding

# BGE-M3 本地部署示例

from FlagEmbedding import BGEM3FlagModel

model = BGEM3FlagModel(‘BAAI/bge-m3’, use_fp16=True)

embeddings = model.encode(

[“这是第一段文本”, “这是第二段文本”],

batch_size=12,

max_length=8192,

)[‘dense_vecs’]

`

4.4 向量数据库对比

向量数据库是存储和检索Embedding向量的专用数据库。2026年的主流选择如下:

类比:向量数据库就像一个按”语义距离”组织的图书馆。传统图书馆按分类号排列,你需要知道分类号才能找到书;语义图书馆按”意思的远近”排列,你只需要描述你在找什么,它就能帮你找到最相关的书。

数据库 类型 分布式 特点 推荐场景

|——–|——|——–|——|———|

Milvus 专用 高性能、云原生、GPU加速 大规模生产环境
Qdrant 专用 Rust实现、过滤能力强 需要复杂过滤的场景
Weaviate 专用 GraphQL接口、多模态 需要丰富API的场景
Chroma 专用 轻量级、开发友好 原型开发、小规模
Pinecone 云服务 全托管、零运维 不想自建基础设施
pgvector 扩展 基于PostgreSQL 已有PG基础设施

`python

# ChromaDB 快速上手示例

import chromadb

# 创建客户端

client = chromadb.PersistentClient(path=”./chroma_db”)

# 创建集合

collection = client.get_or_create_collection(

name=”knowledge_base”,

metadata={“hnsw:space”: “cosine”} # 使用余弦相似度

)

# 添加文档

collection.add(

documents=[“Harness Engineering是一种AI工程方法论”,

“Context层负责管理LLM的上下文信息”],

ids=[“doc1”, “doc2”],

metadatas=[{“source”: “article-01”}, {“source”: “article-04”}]

)

# 查询

results = collection.query(

query_texts=[“什么是Harness Engineering?”],

n_results=3,

where={“source”: {“$eq”: “article-01”}} # 元数据过滤

)

`

4.5 检索策略优化

单纯的向量相似度搜索往往不够。实际工程中需要多种检索策略的组合。

4.5.1 混合搜索(Hybrid Search)

结合关键词搜索和语义搜索的优势:

`python

def hybrid_search(query: str, alpha: float = 0.7) -> list[dict]:

“””混合搜索:语义搜索 + 关键词搜索”””

# 语义搜索结果

semantic_results = vector_db.search(

embedding=get_embedding(query),

top_k=20

)

# 关键词搜索结果(BM25)

keyword_results = bm25_search(query, top_k=20)

# 融合评分 (Reciprocal Rank Fusion)

combined = {}

for rank, doc in enumerate(semantic_results):

combined[doc.id] = combined.get(doc.id, 0) + alpha / (60 + rank)

for rank, doc in enumerate(keyword_results):

combined[doc.id] = combined.get(doc.id, 0) + (1-alpha) / (60 + rank)

# 按融合分数排序

return sorted(combined.items(), key=lambda x: x[1], reverse=True)[:10]

`

4.5.2 重排序(Reranking)

在初筛后用更精细的模型重新排序:

`python

from cohere import Client

co = Client(“your-api-key”)

def rerank(query: str, documents: list[str], top_n: int = 5) -> list[dict]:

“””使用Cohere Reranker重排序”””

results = co.rerank(

query=query,

documents=documents,

top_n=top_n,

model=”rerank-v3.5″

)

return [

{“text”: documents[r.index], “score”: r.relevance_score}

for r in results.results

]

`

4.5.3 查询改写(Query Rewriting)

用LLM改写用户问题,提高检索质量:

`python

def rewrite_query(original_query: str) -> list[str]:

“””查询改写:生成多个检索查询”””

response = llm.chat(f”””

给定以下用户问题,生成3个不同角度的检索查询,用于向量数据库搜索。

只返回查询,每行一个。

用户问题:{original_query}

“””)

return response.strip().split(“n”)

# 生成多个查询 → 分别检索 → 合并去重 → 重排序

queries = rewrite_query(“Agent的记忆机制怎么设计?”)

all_results = []

for q in queries:

all_results.extend(vector_db.search(get_embedding(q), top_k=5))

final_results = deduplicate_and_rerank(all_results)

`


五、长上下文管理

5.1 128K/1M上下文的实际使用

随着Gemini 1.5 Pro支持1M tokens、Claude支持200K tokens,一个自然的问题是:既然窗口这么大了,还需要RAG吗?

答案是:需要,但用法变了

类比:就像你有了一张更大的桌子,但桌子再大也不等于图书馆。大桌子的好处是你可以一次性摊开更多的文件做对比分析,但你仍然需要一个高效的系统来决定哪些文件应该上桌。

长上下文的实际成本

`python

# 计算一次API调用的成本

def estimate_cost(input_tokens: int, output_tokens: int, model: str) -> float:

pricing = {

“gpt-4o”: {“input”: 2.50, “output”: 10.00}, # per 1M tokens

“claude-sonnet-4”: {“input”: 3.00, “output”: 15.00},

“gemini-1.5-pro”: {“input”: 1.25, “output”: 5.00},

}

p = pricing[model]

return (input_tokens * p[“input”] + output_tokens * p[“output”]) / 1_000_000

# 对比:RAG vs 长上下文

rag_cost = estimate_cost(4_000, 500, “gpt-4o”) # ~$0.015

long_ctx_cost = estimate_cost(120_000, 500, “gpt-4o”) # ~$0.305

print(f”RAG成本: ${rag_cost:.4f}”)

print(f”长上下文成本: ${long_ctx_cost:.4f}”)

print(f”长上下文贵 {long_ctx_cost/rag_cost:.1f} 倍”)

# 输出:长上下文贵 ~20倍

`

5.2 上下文压缩技术

当信息量超出窗口限制,或者想要降低成本时,上下文压缩是必要的。

类比:上下文压缩就像做会议纪要——你不会把两小时的会议录音一字不差地记下来,而是提取关键信息、决策和待办事项。

`python

def compress_context(context: str, query: str, max_tokens: int = 4000) -> str:

“””使用LLM压缩上下文,保留与查询相关的关键信息”””

response = llm.chat(f”””

请从以下上下文中提取与问题最相关的关键信息。

输出应该精简但完整,不超过{max_tokens} tokens。

问题:{query}

上下文:

{context}

请提取关键信息(保留原文中的重要细节和数据):

“””)

return response

`

压缩策略对比

策略 压缩率 信息保留率 适用场景

|——|——–|———–|———|

LLM摘要 70-80% ⭐⭐⭐⭐ 通用文档压缩
关键句提取 50-70% ⭐⭐⭐ 长文档快速压缩
基于查询的压缩 80-90% ⭐⭐⭐⭐⭐ 明确查询的场景
结构化提取 60-80% ⭐⭐⭐⭐ 表格、JSON等结构化数据

5.3 滑动窗口策略

对于长对话,使用滑动窗口管理对话历史:

`python

class SlidingWindowContext:

def __init__(self, max_tokens: int = 8000, preserve_recent: int = 5):

self.messages = []

self.max_tokens = max_tokens

self.preserve_recent = preserve_recent

def add_message(self, role: str, content: str):

self.messages.append({“role”: role, “content”: content})

self._trim()

def _trim(self):

“””保留最近N条消息 + 尽可能多的历史消息”””

total_tokens = sum(count_tokens(m[“content”]) for m in self.messages)

if total_tokens <= self.max_tokens:

return

# 保留最近的消息

recent = self.messages[-self.preserve_recent:]

older = self.messages[:-self.preserve_recent]

# 从旧到新保留,直到超出限制

preserved = []

tokens = sum(count_tokens(m[“content”]) for m in recent)

for msg in reversed(older):

msg_tokens = count_tokens(msg[“content”])

if tokens + msg_tokens > self.max_tokens – 500: # 留500 token缓冲

break

preserved.insert(0, msg)

tokens += msg_tokens

# 添加摘要标记

if len(preserved) < len(older):

summary = summarize_messages(older[:len(older)-len(preserved)])

self.messages = [{“role”: “system”, “content”: f”[历史摘要] {summary}”}] + preserved + recent

else:

self.messages = preserved + recent

`


六、动态上下文注入

6.1 基于任务类型的上下文选择

不同的任务需要不同的上下文。动态上下文注入的核心是根据任务类型,智能选择和组装上下文

类比:这就像一个专业的助手在给老板准备会议材料。财务会议准备财务报表,技术会议准备技术方案,客户会议准备客户资料——不同的场景,不同的材料。

`python

from enum import Enum

from typing import Optional

class TaskType(Enum):

QA = “question_answering” # 知识问答

SUMMARIZE = “summarization” # 文档总结

CODE = “code_generation” # 代码生成

ANALYSIS = “data_analysis” # 数据分析

CREATIVE = “creative_writing” # 创意写作

TRANSLATE = “translation” # 翻译

class ContextBuilder:

“””动态上下文组装器”””

# 不同任务类型的上下文模板

TEMPLATES = {

TaskType.QA: {

“system_prompt”: “你是一个知识渊博的助手,请基于提供的参考资料回答问题。”,

“retrieval_top_k”: 5,

“include_history”: True,

“max_context_tokens”: 6000,

},

TaskType.CODE: {

“system_prompt”: “你是一个专业的程序员,请根据需求和参考代码编写代码。”,

“retrieval_top_k”: 3,

“include_history”: False,

“max_context_tokens”: 8000,

},

TaskType.SUMMARIZE: {

“system_prompt”: “请总结以下内容的要点。”,

“retrieval_top_k”: 0, # 不需要检索,直接用原文

“include_history”: False,

“max_context_tokens”: 16000,

},

}

def build_context(

self,

query: str,

task_type: TaskType,

history: Optional[list] = None,

extra_docs: Optional[list] = None

) -> dict:

template = self.TEMPLATES.get(task_type, self.TEMPLATES[TaskType.QA])

context_parts = []

# 1. 系统指令

context_parts.append((“[SYSTEM]”, template[“system_prompt”]))

# 2. 检索相关内容

if template[“retrieval_top_k”] > 0:

docs = self.retriever.search(query, top_k=template[“retrieval_top_k”])

context_parts.append((“[RETRIEVED]”, “n—n”.join(docs)))

# 3. 对话历史

if template[“include_history”] and history:

history_text = self._format_history(history[-10:]) # 最近10轮

context_parts.append((“[HISTORY]”, history_text))

# 4. 额外文档

if extra_docs:

context_parts.append((“[DOCUMENTS]”, “n—n”.join(extra_docs)))

return self._assemble(context_parts, template[“max_context_tokens”])

`

6.2 上下文优先级排序

当上下文空间有限时,需要对候选内容按优先级排序:

`python

class PriorityRanker:

“””上下文优先级排序器”””

# 优先级权重

WEIGHTS = {

“relevance”: 0.40, # 与查询的相关性

“recency”: 0.20, # 信息的新鲜度

“authority”: 0.15, # 信息来源的权威性

“diversity”: 0.15, # 信息的多样性

“completeness”: 0.10, # 信息的完整性

}

def rank(self, candidates: list[dict], query: str) -> list[dict]:

scored = []

for doc in candidates:

score = (

self.WEIGHTS[“relevance”] * self._relevance_score(doc, query) +

self.WEIGHTS[“recency”] * self._recency_score(doc) +

self.WEIGHTS[“authority”] * self._authority_score(doc) +

self.WEIGHTS[“diversity”] * self._diversity_score(doc, scored) +

self.WEIGHTS[“completeness”] * self._completeness_score(doc)

)

scored.append((score, doc))

scored.sort(reverse=True, key=lambda x: x[0])

return [doc for _, doc in scored]

`

6.3 上下文缓存优化

频繁检索相同的文档会浪费资源。缓存是提高效率的关键:

`python

import hashlib

import time

from functools import lru_cache

class ContextCache:

“””上下文缓存管理器”””

def __init__(self, ttl: int = 3600):

self.cache = {}

self.ttl = ttl # 缓存过期时间(秒)

def _key(self, query: str, params: dict) -> str:

content = f”{query}:{sorted(params.items())}”

return hashlib.md5(content.encode()).hexdigest()

def get(self, query: str, params: dict) -> Optional[list]:

key = self._key(query, params)

if key in self.cache:

entry = self.cache[key]

if time.time() – entry[“time”] < self.ttl:

return entry[“data”]

else:

del self.cache[key] # 过期删除

return None

def set(self, query: str, params: dict, data: list):

key = self._key(query, params)

self.cache[key] = {

“data”: data,

“time”: time.time()

}

`

OpenAI的Prompt Caching:2026年,OpenAI等提供商已经支持Prompt级别的自动缓存。如果连续请求的前缀相同(比如相同的System Prompt),会自动命中缓存,减少延迟和成本:

`python

# 连续请求相同的System Prompt时,自动享受缓存折扣

# 第二次起,前缀部分的成本降低50-90%

response = client.chat.completions.create(

model=”gpt-4o”,

messages=[

{“role”: “system”, “content”: LONG_SYSTEM_PROMPT}, # 这部分会被缓存

{“role”: “user”, “content”: user_query} # 这部分每次不同

]

)

`


七、实战:构建一个文档问答系统

现在,让我们把前面学到的所有知识整合起来,构建一个完整的文档问答系统

7.1 系统架构

`

┌──────────────────────────────────────────────────────────┐

│ 文档问答系统架构 │

│ │

│ ┌──────────┐ ┌──────────────┐ ┌───────────────┐ │

│ │ 文档导入 │───▶│ 分块+向量化 │───▶│ 向量数据库 │ │

│ │ PDF/TXT │ │ Embedding │ │ ChromaDB │ │

│ └──────────┘ └──────────────┘ └───────┬───────┘ │

│ │ │

│ ┌──────────┐ ┌──────────────┐ ┌───────▼───────┐ │

│ │ 用户查询 │───▶│ 查询处理 │───▶│ 混合检索 │ │

│ │ Web/API │ │ 改写+Embed │ │ 语义+关键词 │ │

│ └──────────┘ └──────────────┘ └───────┬───────┘ │

│ │ │

│ ┌──────────┐ ┌──────────────┐ ┌───────▼───────┐ │

│ │ 回答输出 │◀───│ LLM推理 │◀───│ 重排序+组装 │ │

│ │ Web/API │ │ GPT-4o │ │ Context Build │ │

│ └──────────┘ └──────────────┘ └───────────────┘ │

└──────────────────────────────────────────────────────────┘

`

7.2 完整代码实现

`python

“””

文档问答系统 – 完整实现

支持:PDF/TXT/MD文档导入、RAG检索、混合搜索、重排序

“””

import os

from dataclasses import dataclass, field

from typing import Optional

from pathlib import Path

import chromadb

from openai import OpenAI

from langchain.text_splitter import RecursiveCharacterTextSplitter

# ==================== 数据结构 ====================

@dataclass

class Document:

“””文档数据结构”””

content: str

metadata: dict = field(default_factory=dict)

embedding: Optional[list[float]] = None

@dataclass

class SearchResult:

“””检索结果”””

content: str

score: float

metadata: dict

# ==================== 文档处理 ====================

class DocumentProcessor:

“””文档处理器:加载、分块、向量化”””

def __init__(self, chunk_size: int = 512, chunk_overlap: int = 64):

self.splitter = RecursiveCharacterTextSplitter(

chunk_size=chunk_size,

chunk_overlap=chunk_overlap,

separators=[“nn”, “n”, “。”, “.”, “!”, “?”, “;”, ” “],

)

self.client = OpenAI()

def load_document(self, file_path: str) -> str:

“””加载文档”””

path = Path(file_path)

if path.suffix == ‘.pdf’:

import PyPDF2

with open(path, ‘rb’) as f:

reader = PyPDF2.PdfReader(f)

return “n”.join(page.extract_text() for page in reader.pages)

else:

return path.read_text(encoding=’utf-8′)

def chunk_document(self, text: str, metadata: dict = None) -> list[Document]:

“””分块处理”””

chunks = self.splitter.split_text(text)

return [

Document(

content=chunk,

metadata={**(metadata or {}), “chunk_index”: i}

)

for i, chunk in enumerate(chunks)

]

def get_embedding(self, text: str) -> list[float]:

“””获取文本的Embedding向量”””

response = self.client.embeddings.create(

input=text,

model=”text-embedding-3-small”

)

return response.data[0].embedding

def process_file(self, file_path: str) -> list[Document]:

“””处理单个文件:加载→分块→向量化”””

text = self.load_document(file_path)

metadata = {“source”: file_path}

chunks = self.chunk_document(text, metadata)

# 批量获取Embedding(效率更高)

texts = [c.content for c in chunks]

embeddings = self._batch_embed(texts)

for doc, emb in zip(chunks, embeddings):

doc.embedding = emb

return chunks

def _batch_embed(self, texts: list[str], batch_size: int = 100) -> list[list[float]]:

“””批量Embedding”””

all_embeddings = []

for i in range(0, len(texts), batch_size):

batch = texts[i:i+batch_size]

response = self.client.embeddings.create(

input=batch,

model=”text-embedding-3-small”

)

all_embeddings.extend([d.embedding for d in response.data])

return all_embeddings

# ==================== 向量存储 ====================

class VectorStore:

“””向量存储:基于ChromaDB”””

def __init__(self, collection_name: str = “knowledge_base”, persist_dir: str = “./chroma_db”):

self.client = chromadb.PersistentClient(path=persist_dir)

self.collection = self.client.get_or_create_collection(

name=collection_name,

metadata={“hnsw:space”: “cosine”}

)

def add_documents(self, documents: list[Document]):

“””添加文档到向量库”””

self.collection.add(

ids=[f”doc_{i}” for i in range(len(documents))],

documents=[d.content for d in documents],

embeddings=[d.embedding for d in documents],

metadatas=[d.metadata for d in documents],

)

def search(self, query_embedding: list[float], top_k: int = 5) -> list[SearchResult]:

“””向量搜索”””

results = self.collection.query(

query_embeddings=[query_embedding],

n_results=top_k,

include=[“documents”, “metadatas”, “distances”]

)

return [

SearchResult(

content=doc,

score=1 – dist, # 余弦距离转相似度

metadata=meta

)

for doc, meta, dist in zip(

results[“documents”][0],

results[“metadatas”][0],

results[“distances”][0]

)

]

# ==================== RAG引擎 ====================

class RAGEngine:

“””RAG问答引擎”””

def __init__(self, vector_store: VectorStore):

self.store = vector_store

self.client = OpenAI()

self.processor = DocumentProcessor()

def ingest(self, file_paths: list[str]):

“””导入文档”””

for path in file_paths:

print(f”处理文档: {path}”)

docs = self.processor.process_file(path)

self.store.add_documents(docs)

print(f” → 分块数: {len(docs)}”)

def query(self, question: str, top_k: int = 5) -> dict:

“””问答查询”””

# 1. 查询Embedding

query_embedding = self.processor.get_embedding(question)

# 2. 向量检索

results = self.store.search(query_embedding, top_k=top_k)

# 3. 组装上下文

context_parts = []

for i, r in enumerate(results, 1):

source = r.metadata.get(“source”, “未知来源”)

context_parts.append(f”[参考{i}] (来源: {source})n{r.content}”)

context = “nn—nn”.join(context_parts)

# 4. LLM生成回答

response = self.client.chat.completions.create(

model=”gpt-4o”,

messages=[

{

“role”: “system”,

“content”: “””你是一个专业的知识助手。请基于提供的参考资料回答用户问题。

规则:

  1. 只基于参考资料回答,不要编造信息
  2. 如果参考资料中没有相关信息,请明确说明
  3. 回答要准确、简洁、有条理
  4. 引用参考资料时标注来源”””

},

{

“role”: “user”,

“content”: f”””参考资料:

{context}

用户问题:{question}

请基于以上参考资料回答问题。”””

}

],

temperature=0.1, # 低温度保证回答一致性

)

return {

“answer”: response.choices[0].message.content,

“sources”: [

{“content”: r.content, “score”: r.score, “metadata”: r.metadata}

for r in results

],

“usage”: {

“input_tokens”: response.usage.prompt_tokens,

“output_tokens”: response.usage.completion_tokens,

}

}

# ==================== 使用示例 ====================

if __name__ == “__main__”:

# 初始化

store = VectorStore()

engine = RAGEngine(store)

# 导入文档

engine.ingest([

“./docs/harness-engineering-01.pdf”,

“./docs/harness-engineering-04.md”,

])

# 查询

result = engine.query(“什么是Harness Engineering?它有哪几层架构?”)

print(“=” * 60)

print(“回答:”)

print(result[“answer”])

print(“n参考来源:”)

for src in result[“sources”]:

print(f” – [{src[‘score’]:.3f}] {src[‘metadata’].get(‘source’, ‘未知’)}”)

`

7.3 运行效果示例

`bash

$ python rag_system.py

处理文档: ./docs/harness-engineering-01.pdf

→ 分块数: 42

处理文档: ./docs/harness-engineering-04.md

→ 分块数: 38

============================================================

回答:

Harness Engineering是一种2026年兴起的AI工程方法论,专注于通过

系统化的”约束”和”引导”来释放大语言模型的潜力。

它包含六层架构:

  1. Prompt层(提示词)- 指令设计与输出格式
  2. Context层(上下文)- 信息管理与RAG
  3. Tool层(工具)- API、MCP与外部能力
  4. Memory层(记忆)- 短期/长期记忆与知识库
  5. Eval层(评估)- 测试、验证与质量保证
  6. Observability层(可观测性)- 监控、日志与追踪

参考来源:

  • [0.923] ./docs/harness-engineering-01.pdf
  • [0.887] ./docs/harness-engineering-04.md
  • [0.856] ./docs/harness-engineering-01.pdf

`


八、常见陷阱与解决方案

在实际工程中,Context层设计有许多容易踩的坑。这里总结最常见的三大陷阱:

8.1 陷阱一:上下文溢出(Context Overflow)

问题:注入的上下文超出模型窗口限制,导致截断或错误。

`

┌──────────────────────────────────────┐

│ 系统指令 2,000 tokens │

│ 对话历史 15,000 tokens │

│ 检索文档 80,000 tokens │

│ 用户问题 200 tokens │

├──────────────────────────────────────┤

│ 总计 97,200 tokens │

│ 模型限制 128,000 tokens │

│ 安全余量 30,800 tokens ✓ │

└──────────────────────────────────────┘

`

解决方案

`python

def safe_context_assembly(parts: list[str], max_tokens: int = 100000) -> str:

“””安全的上下文组装,确保不超出限制”””

total = 0

assembled = []

for part in parts:

part_tokens = count_tokens(part)

if total + part_tokens > max_tokens:

# 截断当前部分以适应限制

remaining = max_tokens – total

if remaining > 100: # 至少保留100 tokens才有意义

truncated = truncate_to_tokens(part, remaining)

assembled.append(truncated + “n[…内容已截断]”)

break

assembled.append(part)

total += part_tokens

return “nn”.join(assembled)

`

8.2 陷阱二:信息稀释(Information Dilution)

问题:注入了太多低相关信息,淹没了真正有用的信息。

类比:这就像你在图书馆找一本关于”量子力学”的书,但管理员给了你一整书架的书——其中只有一两本是关于量子力学的,其他都是关于古典文学的。信息量很大,但有效信息被稀释了。

解决方案

`python

def filter_by_relevance(results: list[SearchResult], threshold: float = 0.7) -> list[SearchResult]:

“””过滤低相关性结果”””

filtered = [r for r in results if r.score >= threshold]

# 如果过滤后结果太少,降低阈值

if len(filtered) < 2:

filtered = sorted(results, key=lambda x: x.score, reverse=True)[:3]

return filtered

def deduplicate_results(results: list[SearchResult], similarity_threshold: float = 0.95) -> list[SearchResult]:

“””去重:移除内容高度相似的结果”””

unique = []

for r in results:

is_dup = False

for u in unique:

if cosine_similarity(r.content, u.content) > similarity_threshold:

is_dup = True

break

if not is_dup:

unique.append(r)

return unique

`

8.3 陷阱三:幻觉(Hallucination)

问题:LLM在上下文信息不足时,倾向于”编造”看似合理但错误的回答。

解决方案

`python

# 方案1:在System Prompt中明确约束

ANTI_HALLUCINATION_PROMPT = “””

你是一个严谨的知识助手。请严格遵守以下规则:

  1. 只基于【参考资料】中的信息回答问题
  2. 如果参考资料中没有相关信息,回答”根据已有资料,我无法回答这个问题”
  3. 不要编造、推测或补充参考资料中没有的信息
  4. 回答时引用具体的参考资料编号,格式:[参考X]
  5. 如果信息可能不完整或有歧义,请明确说明

“””

# 方案2:添加置信度评估

def query_with_confidence(question: str, context: str) -> dict:

response = llm.chat(f”””

基于以下参考资料回答问题,并评估你的置信度。

参考资料:{context}

问题:{question}

请以JSON格式回答:

{{

“answer”: “你的回答”,

“confidence”: 0.0-1.0,

“sources_used”: [“参考1”, “参考2”],

“gaps”: “信息不足之处(如有)”

}}

“””)

return json.loads(response)

`

陷阱速查表

陷阱 表现 解决方案 预防措施

|——|——|———|———|

上下文溢出 API报错、回答截断 Token计数+截断 设定安全上限
信息稀释 回答笼统、不准确 相关性过滤+重排序 提高检索精度
幻觉 编造不存在的信息 约束Prompt+置信度评估 提供充足上下文
检索偏差 检索到错误文档 混合搜索+查询改写 优化Embedding
重复内容 同一信息多次出现 去重算法 分块策略优化
延迟过高 响应时间>5秒 缓存+异步处理 架构优化

九、总结

9.1 核心要点回顾

`

┌─────────────────────────────────────────────────────────┐

│ Context层设计要点 │

│ │

│ 1. Context是Agent的工作记忆,决定回答质量的上限 │

│ 2. 三大策略:RAG + 长上下文 + 动态注入,通常混合使用 │

│ 3. RAG四要素:分块策略 × Embedding模型 × 向量数据库 × │

│ 检索策略 │

│ 4. 长上下文不等于万能,成本和注意力衰减是主要限制 │

│ 5. 动态注入根据任务类型智能组装最优上下文 │

│ 6. 避免三大陷阱:溢出、稀释、幻觉 │

└─────────────────────────────────────────────────────────┘

`

9.2 Context层设计决策树

`

你的场景是什么?

├─ 需要查询大量文档/知识库?

│ ├─ 文档量 < 100页 → 直接塞入长上下文

│ └─ 文档量 > 100页 → RAG方案

│ ├─ 需要精确匹配 → 混合搜索(语义+关键词)

│ └─ 需要语义理解 → 语义搜索 + 重排序

├─ 需要长文档分析?

│ ├─ 文档 < 模型窗口 → 直接使用

│ └─ 文档 > 模型窗口 → 分段处理 + 摘要链

└─ 需要多轮复杂对话?

├─ 轮次 < 20轮 → 完整历史

└─ 轮次 > 20轮 → 滑动窗口 + 摘要压缩

`

9.3 下篇预告

在下一篇《AI PRO·Harness Day 5 Tool层设计:Memory层设计》中,我们将深入探讨:

  • 短期记忆 vs 长期记忆:Agent如何记住和遗忘?
  • 记忆的存储与检索:如何高效管理Agent的记忆库?
  • 记忆的衰减与强化:如何模拟人类的记忆机制?
  • 多Agent记忆共享:多个Agent之间如何共享知识?
  • 实战:构建一个具有长期记忆的个人助理

Context层解决的是”当前对话需要什么知识”,而Memory层要解决的是”Agent如何积累和运用长期经验”。这两层紧密配合,共同构成了Agent的”认知基础设施”。

敬请期待!


系列文章

第一篇:什么是Harness Engineering

第二篇:六层架构详解

第三篇:Prompt层设计

第四篇:Context层设计 ← 你在这里

第五篇:Tool层设计

第六篇:Memory层设计

第七篇:Eval层设计

第八篇:Observability层设计


*本文约10,000字,阅读时间约25分钟。如有疑问或建议,欢迎在评论区交流。*

评论

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注