0Pricing
AI Engineering Academy · 课时

查询、检索与生成

编写查询流程,为用户问题生成嵌入,检索排名前 k 的分块,格式化增强提示,调用 LLM,并返回带引用的答案。

查询、检索与生成 是 CoddyKit 上的免费 AI Engineering Academy 课时。 这是第 4 节课,共 4 节。 你可以在下方免费阅读本课时的完整内容 — 然后在浏览器中使用内置代码编辑器和全天候 AI 导师进行实践。 这是 AI Engineering Academy 学习路径的一部分,你的进度在网页和 CoddyKit 应用中同步。 AI Engineering Academy 课程共包含 4 节课。

查询流程:端到端

查询流程是RAG 的在线部分,也就是用户提出问题时实时运行的代码。它连接了建立 Index 期间构建的所有组件:嵌入模型、向量存储、提示模板和 LLM。实现良好的查询流程在大多数工作负载下都能在 500 毫秒内完成,并生成有依据且带引用的答案。本课将从零开始构建每个步骤。

步骤 1:嵌入用户查询

第一步是使用建立 Index 时采用的同一个模型,将用户的自然语言问题转换为向量嵌入。这个嵌入会编码问题的语义,并与向量存储中的文档分块嵌入进行比较。请让这一步保持快速——使用 text-embedding-3-small 之类的轻量模型,并缓存重复相同查询的嵌入。

from openai import OpenAI

client = OpenAI()

def embed_query(question: str) -> list:
    response = client.embeddings.create(
        model='text-embedding-3-small',
        input=[question]
    )
    return response.data[0].embedding

user_question = 'What is our remote work policy?'
query_vector = embed_query(user_question)
print(f'Query embedded: {len(query_vector)}-dim vector')

步骤 2:检索前 K 个分块

将查询向量发送到向量存储,以查找语义最相似的 K 个分块。返回的匹配结果会按照余弦相似度分数排序(通常为 0.0 到 1.0,数值越高越好)。理想的 K 值需要在上下文丰富度与上下文窗口成本之间取得平衡:K=5 是常见的起始值。您还可以在此处应用元数据筛选条件,将检索范围限制在特定部门、文档类型或日期范围内。

def retrieve_chunks(query_vector, index, top_k=5, filters=None):
    query_params = {
        'vector': query_vector,
        'top_k': top_k,
        'include_metadata': True
    }
    if filters:
        query_params['filter'] = filters

    results = index.query(**query_params)

    chunks = []
    for match in results.matches:
        chunks.append({
            'score': match.score,
            'text': match.metadata['text'],
            'source': match.metadata.get('source', ''),
            'page': match.metadata.get('page', '')
        })
    return chunks

步骤 3:按分数阈值筛选

并非所有检索到的分块都真正相关——有些分块的相似度分数可能很低,但由于查询超出了 Index 的覆盖范围,仍然排在前 K 个结果中。请应用最低分数阈值,筛除置信度较低的匹配结果。如果所有检索到的分块都低于该阈值,应返回“未找到相关信息”,而不是将无关上下文发送给 LLM;后者生成的答案会比礼貌地拒绝回答更糟。

MIN_SCORE_THRESHOLD = 0.75

def filter_by_score(chunks, threshold=MIN_SCORE_THRESHOLD):
    relevant = [c for c in chunks if c['score'] >= threshold]
    if not relevant:
        print(f'No chunks above threshold {threshold}. Scores: {[c["score"] for c in chunks]}')
    return relevant

retrieved = retrieve_chunks(query_vector, index, top_k=5)
filtered = filter_by_score(retrieved)
if not filtered:
    print('Responding: no relevant information found')

步骤 4:格式化上下文块

将检索到的分块组装成 LLM 将要读取的结构化上下文块。为每个分块标注来源,以便模型准确引用。请在分块之间添加分隔符,提升可读性。将总上下文控制在令牌预算以内——使用 tiktoken 统计令牌数;如果超出限制,则截断或删除分数较低的分块。上下文块会插入提示中,位置在系统指令与用户问题之间。

def format_context(chunks):
    parts = []
    for i, chunk in enumerate(chunks, start=1):
        source_label = chunk['source']
        if chunk.get('page'):
            source_label += f", page {chunk['page']}"
        parts.append(
            f'[Document {i} | Source: {source_label}]\n{chunk["text"]}'
        )
    return '\n\n---\n\n'.join(parts)

context = format_context(filtered)
print(f'Context block: {len(context)} characters')

步骤 5:构建增强提示

将上下文块、系统指令和用户问题组合成最终提示。系统消息会告诉模型只能使用提供的上下文,并且要引用来源。用户消息包含格式化后的上下文,后面接着问题。这种清晰的分隔可以防止模型将上下文内容与问题混淆,也明确划分了检索数据和用户输入之间的边界。

def build_prompt(question, context):
    system_message = (
        'You are a helpful assistant. Answer the question using ONLY '
        'the information in the provided documents. '
        'Cite the document number(s) used, like [Doc 1]. '
        'If the documents do not contain the answer, say so.'
    )
    user_message = (
        f'Documents:\n\n{context}\n\n'
        f'Question: {question}'
    )
    return system_message, user_message

步骤 6:调用 LLM 并获取答案

使用 Chat Completions API 将组装好的提示发送给 LLM。在事实性问答中使用较低的温度(0.0 到 0.3),以获得一致且有依据的答案。较高的温度会产生更具创造性的回答,但也会增加模型添加上下文之外信息的风险。解析响应,同时返回答案文本和检索到的来源,以便应用向用户展示引用。

def generate_answer(question, context, sources):
    system_msg, user_msg = build_prompt(question, context)

    response = client.chat.completions.create(
        model='gpt-4o',
        temperature=0.1,   # low temperature for factual Q&A
        messages=[
            {'role': 'system', 'content': system_msg},
            {'role': 'user', 'content': user_msg}
        ]
    )
    answer = response.choices[0].message.content
    return {
        'answer': answer,
        'sources': sources,
        'tokens_used': response.usage.total_tokens
    }

整合全部步骤

完整的查询流程会依次调用这些步骤。每一步都是可以独立测试的纯函数,数据也会从一个步骤清晰地流向下一个步骤。在每一步添加日志记录,可以让流程具备可观测性——您能够准确查看检索到了哪些分块、它们的分数是多少、上下文是如何组装的,以及使用了多少令牌。这种可见性对于调试和提升检索质量至关重要。

def answer_question(user_question, vector_index):
    # Step 1: Embed query
    q_vector = embed_query(user_question)

    # Step 2: Retrieve
    chunks = retrieve_chunks(q_vector, vector_index, top_k=5)

    # Step 3: Filter low-confidence matches
    chunks = filter_by_score(chunks, threshold=0.70)
    if not chunks:
        return {'answer': 'I do not have information about that topic.', 'sources': []}

    # Step 4 & 5: Format and build prompt
    context = format_context(chunks)
    sources = [c['source'] for c in chunks]

    # Step 6: Generate
    return generate_answer(user_question, context, sources)

延迟优化

查询流程包含两个受输入/输出限制的步骤:嵌入调用和 LLM 调用。请让它们避免不必要的等待:嵌入调用速度很快(<100ms),LLM 调用则较慢(500ms-3s)。为了降低用户感知到的延迟,请流式传输 LLM 响应,让令牌在生成后立即显示,而不是等待完整响应。对于重复的相同查询,请缓存其嵌入,以避免重复调用 API。

async def answer_question_streaming(question, index):
    q_vector = embed_query(question)
    chunks = retrieve_chunks(q_vector, index, top_k=5)
    chunks = filter_by_score(chunks)
    if not chunks:
        yield 'I do not have information about that topic.'
        return
    context = format_context(chunks)
    system_msg, user_msg = build_prompt(question, context)

    stream = await client.chat.completions.create(
        model='gpt-4o',
        stream=True,
        messages=[
            {'role': 'system', 'content': system_msg},
            {'role': 'user', 'content': user_msg}
        ]
    )
    async for chunk in stream:
        delta = chunk.choices[0].delta.content or ''
        yield delta

通过日志记录实现可观测性

生产环境中的 RAG 流程需要结构化日志记录,以便您诊断检索失败或 LLM 生成错误答案的情况。请为每个请求记录查询、检索到的分块 ID 和分数、上下文令牌数、答案以及延迟。将这些日志存储在数据库或可观测性平台中。当用户报告错误答案时,您可以重放完全相同的查询,并检查检索到了哪些分块,以及它们为何不足以支持答案。

import time
import logging
import json

def answer_question_with_logging(question, index):
    start = time.time()
    q_vector = embed_query(question)
    chunks = retrieve_chunks(q_vector, index, top_k=5)
    chunks = filter_by_score(chunks)
    context = format_context(chunks)
    result = generate_answer(question, context, [c['source'] for c in chunks])
    latency_ms = (time.time() - start) * 1000
    log_entry = {
        'question': question,
        'num_chunks_retrieved': len(chunks),
        'chunk_scores': [c['score'] for c in chunks],
        'tokens_used': result.get('tokens_used'),
        'latency_ms': round(latency_ms)
    }
    logging.info(json.dumps(log_entry))
    return result

缓存查询嵌入

如果您的应用收到大量重复或近似相同的查询——例如 FAQ 机器人中用户经常提出相同问题——那么缓存查询嵌入是一种简单且效果显著的优化方式。对查询字符串进行哈希处理,在 Redis 缓存中查找对应的嵌入,只有缓存未命中时才调用嵌入 API。在生产环境的 FAQ 和支持聊天机器人中,嵌入缓存命中率达到 30-60% 很常见;这可以大幅减少 API 成本,并让每次命中缓存的查询减少 50-100 毫秒的延迟。

import hashlib
import json
import redis

r = redis.Redis(host='localhost', port=6379)
EMBED_CACHE_TTL = 86400  # 24 hours

def embed_query_cached(question):
    cache_key = 'embed:' + hashlib.sha256(question.encode()).hexdigest()
    cached = r.get(cache_key)
    if cached:
        return json.loads(cached)  # cache hit
    # Cache miss: call the API
    vector = embed_query(question)
    r.setex(cache_key, EMBED_CACHE_TTL, json.dumps(vector))
    return vector

快速检查

测试您对本课 AI 工程概念的理解。

课程回顾

本课您学习了:六步查询流程(嵌入查询、检索分块、按分数筛选、格式化上下文、构建提示、生成答案);使用分数阈值筛选处理超出 Index 覆盖范围的查询;以及包括响应流式传输、结构化日志记录和延迟优化在内的生产环境增强措施。接下来,我们将学习如何评估完整的 RAG 系统是否真正正常工作。

常见问题解答

「查询、检索与生成」课时是免费的吗?

是的 — 「查询、检索与生成」的完整文本可在网页上免费阅读。要进行交互式练习(内置代码编辑器和全天候 AI 导师)并解锁 AI Engineering Academy 课程的其余内容,请升级到 CoddyKit PRO。 AI Engineering Academy 课程共包含 4 节课。

「查询、检索与生成」这节课中我会学到什么?

编写查询流程,为用户问题生成嵌入,检索排名前 k 的分块,格式化增强提示,调用 LLM,并返回带引用的答案。 你通过在浏览器中直接运行的动手代码来练习 AI Engineering Academy,全天候 AI 导师会在你学习这节课的过程中回答你的问题。

学习 AI Engineering Academy 需要有经验吗?

无需任何先前经验。CoddyKit 上的 AI Engineering Academy 课程适合初学者到高级学习者,你可以从这里开始或从头开始,按照自己的节奏学习。 这是第 4 节课,共 4 节。

「查询、检索与生成」课时需要多长时间?

大多数 CoddyKit 课程大约需要 5–10 分钟。每节课都很精短且互动,所以你能稳步进步,并在网页和应用中从离开的地方继续。

我能在这节 AI Engineering Academy 课中编写并运行代码吗?

能。每节 AI Engineering Academy 课都包含内置代码编辑器,你可以在浏览器中直接编写并运行真实代码,并获得即时 AI 反馈 — 无需本地设置。

此课程中的所有课时

  1. 文档加载与文本提取
  2. 分块策略:固定大小、按句子与递归
  3. 索引:嵌入并存储分块
  4. 查询、检索与生成
← 返回 AI Engineering Academy