Learning

Record learning from practice

View project on GitHub

长文本信息稀释原因及可能方向

参考代码1, 参考代码2

实际影响

场景1:完美匹配但得分低

问题: "如何配置DDR4内存时序?"
Chunk: "第5章 内存子系统...
       5.1 DDR4架构
       5.2 时序配置详解
           CAS延迟设置...
           RAS to CAS延迟...
       5.3 错误检测
       5.4 性能优化
       5.5 兼容性列表"

预期相似度: 0.85+
实际相似度: 0.42  ← 因为chunk还包含架构、错误检测、性能、兼容性

场景2:部分相关但得分相近

问题: "DDR4时序配置"
Chunk A: "DDR4时序配置详细步骤..."  (150 tokens, 聚焦)
Chunk B: "内存全面介绍: DDR3/DDR4/DDR5, 时序/容量/频率..." (500 tokens)

Chunk A相似度: 0.52
Chunk B相似度: 0.48  ← 几乎一样!但B才150字讨论DDR4时序

优化策略

  • 添加特殊引导词
  • 分层检索
  • 利用”指令遵循”能力
  • 调整Chunk大小
  • 多力度Embedding
  • 晚期交互
  • 混合检索

策略1: 添加特殊提示词引导(推荐)

Decoder模型对prompt敏感,可以在每个chunk前加提示:

class EmbeddingFunction:
    def __call__(self, input: Documents) -> Embeddings:
        all_embeddings = []
        for doc in input:
            prompted_doc = f"Document summary: {doc}"
            # prompted_doc = f"This text discusses: {doc}"
            
            response = self.embed(prompted_doc)
            embeddings = [it["embedding"] for it in response.get("data", [])]
            if embeddings:
                all_embeddings.append(embeddings[0])
        return all_embeddings

策略2: 后处理 - 添加查询端优化

在查询时,也添加相同的格式:

def retrieve(self, query: str, n_results: int = 3):
    formatted_query = f"Query: {query}"
    
    results = self.collection.query(
        query_texts=[formatted_query],
        n_results=n_results
    )

策略3: 分层检索(Hierarchical Retrieval)

利用Decoder的优势,实现两阶段检索:

def hierarchical_retrieve(chunk, query):
    """
    第一阶段:句子级检索
    """
    sentences = split_sentences(chunk)
    sentence_embeddings = []
    
    for sent in sentences:
        # Decoder对每个句子生成embedding
        # 句子短,不会被稀释
        emb = decoder_embed(sent)
        sentence_embeddings.append(emb)
    
    # 找到最相关的句子
    similarities = [cosine_sim(query_emb, sent_emb) 
                   for sent_emb in sentence_embeddings]
    
    best_sentence_idx = argmax(similarities)
    
    """
    第二阶段:上下文扩展
    """
    # 返回最相关句子及其上下文
    context_window = 2  # 前后各2句
    start = max(0, best_sentence_idx - context_window)
    end = min(len(sentences), best_sentence_idx + context_window + 1)
    
    return ' '.join(sentences[start:end])

策略4: 利用Decoder的”指令遵循”能力

Qwen模型通常有指令遵循能力,可以这样使用:

# 为chunk生成"摘要式"embedding
def create_focused_embedding(chunk):
    # 方式1: 直接摘要
    summary_prompt = f"Summarize the key points of this text: {chunk}"
    summary_embedding = decoder_embed(summary_prompt)
    
    # 方式2: 提取关键词
    keywords_prompt = f"Extract main keywords from: {chunk}"
    keywords_embedding = decoder_embed(keywords_prompt)
    
    # 方式3: 混合
    combined_embedding = 0.5 * summary_embedding + 0.5 * keywords_embedding
    
    return combined_embedding

策略5: 调整Chunk策略(治本)

针对Decoder模型的特性优化分块:

# 更激进的分块策略
chunk_size = 800  # 从2000降到800(Decoder能更好地"记住")
chunk_overlap = 200  # 增加overlap确保上下文完整

# 按语义单元分块
def semantic_chunking(text):
    """
    不按固定长度,而是按语义完整性分块
    """
    sections = split_by_headers(text)  # 按标题分
    
    chunks = []
    for section in sections:
        if len(section) < 1500:
            chunks.append(section)  # 保持完整
        else:
            # 过长的section再细分
            sub_chunks = split_by_paragraphs(section, max_len=800)
            chunks.extend(sub_chunks)
    
    return chunks

策略6: 多粒度Embedding

同时存储不同粒度的embedding:

class MultiGranularityStore:
    def add_document(self, doc):
        # 粒度1: 整个文档
        doc_embedding = embed(doc)
        
        # 粒度2: 段落级别
        paragraphs = split_paragraphs(doc)
        para_embeddings = [embed(p) for p in paragraphs]
        
        # 粒度3: 句子级别
        sentences = split_sentences(doc)
        sent_embeddings = [embed(s) for s in sentences]
        
        # 存储时关联起来
        self.store({
            'doc_id': doc_id,
            'doc_embedding': doc_embedding,
            'paragraphs': [
                {'text': p, 'embedding': e, 'parent': doc_id}
                for p, e in zip(paragraphs, para_embeddings)
            ],
            'sentences': [
                {'text': s, 'embedding': e, 'parent': para_id}
                for s, e in zip(sentences, sent_embeddings)
            ]
        })
    
    def retrieve(self, query, strategy='adaptive'):
        if strategy == 'adaptive':
            # 先用句子级检索找到候选
            top_sentences = self.search_sentences(query, top_k=20)
            
            # 然后扩展到段落
            paragraphs = [self.get_paragraph(s['parent']) 
                         for s in top_sentences]
            
            # 最后用段落级embedding重排序
            reranked = self.rerank_by_paragraph_embedding(query, paragraphs)
            
            return reranked[:3]

策略7: 晚期交互 (Late Interaction)

# ColBERT风格:保留token级别的表示
query_tokens = embed_each_token(query)  # [10, 768]
chunk_tokens = embed_each_token(chunk)  # [500, 768]

# 计算每个query token与最相关的chunk token的相似度
similarity = sum([
    max([cos_sim(q_tok, c_tok) for c_tok in chunk_tokens])
    for q_tok in query_tokens
])

策略8: 混合检索

# 同时使用
1. 语义搜索 (embedding)
2. 关键词搜索 (BM25)   不受长度影响
3. 融合结果

建议

短期优化:

  1. 减小chunk_size: 从2000 → 800
  2. 增加chunk_overlap: 从200 → 300
  3. 在评估系统中验证效果

中期优化:

  1. 实现句子级检索
  2. 添加查询/文档的格式化prompt
  3. 优化分块策略(按语义而非长度)

长期优化:

  1. 多粒度embedding架构
  2. 混合检索(BM25 + Semantic)
  3. 微调Qwen模型(在你的领域数据上)

总结

虽然Decoder模型在架构上有优势:

  • ✅ 最后一个token天然包含全局信息
  • ✅ 可以处理更长的序列
  • ✅ 语义理解能力更强

但长文本的稀释问题仍然存在

  • ❌ 多主题混合导致每个主题的表达被稀释
  • ❌ 可能有末尾位置偏见