第27章 框架实操:用 LangChain/LlamaIndex 搭建完整 RAG 系统

第27章 框架实操:用 LangChain/LlamaIndex 搭建完整 RAG 系统

大白话一句话: 前面六章学了RAG的每个零件,这章把它们组装成一台能跑的"整车"------用LangChain和LlamaIndex两个框架,从零搭建一个生产级RAG系统。


27.1 为什么需要框架

27.1.1 不用框架的痛

前面几章的代码都是"手写"的------自己写文档加载、自己写切分、自己写检索、自己写生成。好处是每一步都看得懂,坏处是:

痛点 具体表现
胶水代码太多 光把各环节串起来就要写几百行
换组件成本高 想从Chroma换成Milvus,检索代码全要改
缺少生产特性 流式输出、重试、缓存、链路追踪都得自己写
维护困难 代码散落各处,改一个地方怕影响全局

27.1.2 框架解决什么

LangChain 和 LlamaIndex 是当前最主流的两个RAG框架,它们的核心价值是:

复制代码
框架 = 标准化接口 + 预置组件 + 可组合管道 + 生产级特性

标准化接口:所有向量数据库用同一套API,换底层只需改一行配置
预置组件:文档加载器、切分器、Embedding、检索器、生成器全部内置
可组合管道:像搭积木一样组装Pipeline,支持链式调用
生产级特性:流式输出、缓存、异步、链路追踪、重试机制开箱即用

27.1.3 LangChain vs LlamaIndex 怎么选

维度 LangChain LlamaIndex
定位 通用LLM应用框架 专注数据/文档的RAG框架
优势 生态大、组件多、Agent能力强 RAG做得更精细、文档处理更强
劣势 偏重、抽象层多、版本变化快 Agent能力相对弱
适合场景 Agent + RAG混合应用 纯RAG/文档问答
学习曲线 中等偏陡 相对平缓
社区活跃度 最高

大白话建议: 纯RAG项目选LlamaIndex,Agent+RAG混合选LangChain。两个都值得了解,本本章两个框架的完整实战都会讲。先学LlamaIndex上手快,再学LangChain扩展性强。


27.2 LlamaIndex 实战:30行代码跑通RAG

27.2.1 环境安装

bash 复制代码
# 基础安装
pip install llama-index llama-index-embeddings-huggingface
pip install llama-index-llms-openai-like  # 兼容OpenAI格式的API
pip install llama-index-vector-stores-chroma chromadb

# 如果用本地模型
pip install llama-index-llms-ollama

27.2.2 最简RAG:30行代码

python 复制代码
from llama_index.core import VectorStoreIndex, SimpleDirectoryReader, Settings
from llama_index.embeddings.huggingface import HuggingFaceEmbedding
from llama_index.llms.openai_like import OpenAILike

# 全局配置
Settings.embed_model = HuggingFaceEmbedding(
    model_name="BAAI/bge-large-zh-v1.5"
)
Settings.llm = OpenAILike(
    model="qwen2.5:7b",
    api_base="http://localhost:11434/v1",
    api_key="not-needed",
    temperature=0.1
)

# 1. 加载文档
documents = SimpleDirectoryReader("./docs").load_data()
print(f"加载了 {len(documents)} 个文档")

# 2. 构建索引(自动切分 + 向量化 + 存储)
index = VectorStoreIndex.from_documents(documents)

# 3. 创建查询引擎
query_engine = index.as_query_engine(
    similarity_top_k=5,        # 检索Top-5
    response_mode="compact",   # 回答模式:紧凑
)

# 4. 提问
response = query_engine.query("Qwen2.5支持多少种语言?")
print(response)

# 5. 查看检索到的来源
for i, node in enumerate(response.source_nodes, 1):
    print(f"[{i}] score={node.score:.3f} | {node.text[:80]}...")

就这30行代码,跑通了一个完整的RAG系统:文档加载→切分→向量化→存储→检索→生成。

27.2.3 LlamaIndex的抽象层次

LlamaIndex提供三个层次的API,从简到繁:

复制代码
高层次(High-Level):30行代码跑通
    └── VectorStoreIndex.from_documents() → query()
    
中层次(Mid-Level):可定制每个环节
    └── ServiceContext + Retriever + ResponseSynthesizer
    
低层次(Low-Level):完全控制
    └── Document + Node + Embedding + VectorStore + Retriever + LLM

生产项目通常用中层次API------既不至于太黑盒,又不用写太多胶水代码。


27.3 LlamaIndex 进阶:定制每个环节

27.3.1 定制文档处理

python 复制代码
from llama_index.core import Document
from llama_index.core.node_parser import (
    SentenceSplitter,
    SemanticSplitterNodeParser,
    MarkdownNodeParser,
)
from llama_index.core.schema import MetadataMode


# ===== 1. 文档加载 =====
def load_documents(directory: str) -> list:
    """加载目录下所有文档"""
    from llama_index.core import SimpleDirectoryReader
    
    reader = SimpleDirectoryReader(
        input_dir=directory,
        required_exts=[".pdf", ".docx", ".md", ".txt", ".html"],
        recursive=True,  # 递归子目录
    )
    documents = reader.load_data()
    
    # 添加全局元数据
    for doc in documents:
        doc.metadata.update({
            "loaded_at": "2024-01-01",
            "project": "AI开发指南",
        })
    
    return documents


# ===== 2. 文档切分 =====
def split_documents(documents: list, strategy: str = "recursive"):
    """
    切分策略:
    - recursive: 递归切分(推荐通用场景)
    - semantic: 语义切分(高质量需求)
    - markdown: Markdown结构切分(有标题的文档)
    """
    if strategy == "recursive":
        splitter = SentenceSplitter(
            chunk_size=400,
            chunk_overlap=60,
            separator="\n\n",
            secondary_chunking_regex="[^,.;。!?]+[,.;。!?]?",
        )
    elif strategy == "semantic":
        splitter = SemanticSplitterNodeParser(
            buffer_size=1,
            breakpoint_percentile_threshold=95,
            embed_model=Settings.embed_model,
        )
    elif strategy == "markdown":
        splitter = MarkdownNodeParser()
    else:
        raise ValueError(f"未知策略: {strategy}")
    
    nodes = splitter.get_nodes_from_documents(documents)
    print(f"切分完成:{len(documents)}个文档 → {len(nodes)}个片段")
    return nodes


# ===== 3. 数据清洗 =====
def clean_nodes(nodes: list) -> list:
    """清洗切分后的节点"""
    import re
    
    cleaned = []
    for node in nodes:
        text = node.text
        
        # 去除多余空白
        text = re.sub(r'\n{3,}', '\n\n', text)
        text = re.sub(r' {2,}', ' ', text)
        
        # 去除URL和邮箱
        text = re.sub(r'http[s]?://\S+', '', text)
        text = re.sub(r'\S+@\S+', '', text)
        
        # 过滤太短的片段
        if len(text.strip()) < 50:
            continue
        
        node.text = text.strip()
        cleaned.append(node)
    
    print(f"清洗完成:{len(nodes)} → {len(cleaned)} 个有效片段")
    return cleaned


# 完整文档处理Pipeline
documents = load_documents("./docs")
nodes = split_documents(documents, strategy="recursive")
nodes = clean_nodes(nodes)

27.3.2 定制向量存储

python 复制代码
from llama_index.vector_stores.chroma import ChromaVectorStore
from llama_index.core import VectorStoreIndex, StorageContext
import chromadb


# ===== 方案一:Chroma(开发推荐) =====
def create_chroma_index(nodes):
    """使用Chroma向量数据库"""
    db = chromadb.PersistentClient(path="./chroma_db")
    chroma_store = ChromaVectorStore(
        collection_name="knowledge_base"
    )
    storage_context = StorageContext.from_defaults(
        vector_store=chroma_store
    )
    index = VectorStoreIndex(
        nodes=nodes,
        storage_context=storage_context,
    )
    return index


# ===== 方案二:PostgreSQL + pgvector(生产推荐) =====
def create_pgvector_index(nodes):
    """使用PgVector向量数据库"""
    from llama_index.vector_stores.postgres import PGVectorStore
    
    vector_store = PGVectorStore.from_params(
        database="rag_db",
        host="localhost",
        port="5432",
        user="postgres",
        password="password",
        table_name="knowledge_base",
        embed_dim=1024,  # BGE-large维度
    )
    
    storage_context = StorageContext.from_defaults(
        vector_store=vector_store
    )
    index = VectorStoreIndex(
        nodes=nodes,
        storage_context=storage_context,
    )
    return index


# ===== 方案三:Milvus(大规模推荐) =====
def create_milvus_index(nodes):
    """使用Milvus向量数据库"""
    from llama_index.vector_stores.milvus import MilvusVectorStore
    
    vector_store = MilvusVectorStore(
        uri="http://localhost:19530",
        collection_name="knowledge_base",
        dim=1024,
        overwrite=False,  # 不覆盖已有数据
    )
    
    storage_context = StorageContext.from_defaults(
        vector_store=vector_store
    )
    index = VectorStoreIndex(
        nodes=nodes,
        storage_context=storage_context,
    )
    return index

27.3.3 定制检索器

python 复制代码
from llama_index.core.retrievers import (
    VectorIndexRetriever,
    QueryFusionRetriever,
)
from llama_index.retrievers.bm25 import BM25Retriever
from llama_index.core.postprocessor import (
    SimilarityPostprocessor,
    KeywordNodePostprocessor,
)


# ===== 基础向量检索器 =====
def create_vector_retriever(index, top_k: int = 10):
    """向量检索器"""
    return VectorIndexRetriever(
        index=index,
        similarity_top_k=top_k,
    )


# ===== 混合检索器(向量 + BM25) =====
def create_hybrid_retriever(index, nodes, top_k: int = 10):
    """混合检索:向量 + BM25,用RRF融合"""
    vector_retriever = VectorIndexRetriever(
        index=index,
        similarity_top_k=top_k,
    )
    bm25_retriever = BM25Retriever.from_defaults(
        nodes=nodes,
        similarity_top_k=top_k,
    )
    
    # RRF融合
    hybrid_retriever = QueryFusionRetriever(
        retrievers=[vector_retriever, bm25_retriever],
        similarity_top_k=top_k,
        num_queries=1,       # 不做查询扩展
        mode="reciprocal_rerank",  # RRF
    )
    return hybrid_retriever


# ===== 后处理器:过滤 + 重排序 =====
def create_postprocessors():
    """后处理Pipeline"""
    return [
        # 1. 过滤低分结果
        SimilarityPostprocessor(similarity_cutoff=0.5),
        # 2. 关键词过滤(排除/要求包含)
        # KeywordNodePostprocessor(
        #     exclude_keywords=["废弃", "已删除"],
        #     require_keywords=[],
        # ),
    ]


# ===== 重排序器 =====
def create_reranker():
    """BGE Reranker重排序"""
    from llama_index.core.postprocessor import SentenceTransformerRerank
    
    return SentenceTransformerRerank(
        model="BAAI/bge-reranker-v2-m3",
        top_n=5,  # 重排后取Top-5
    )

27.3.4 完整定制Pipeline

python 复制代码
from llama_index.core import get_response_synthesizer
from llama_index.core.query_engine import RetrieverQueryEngine
from llama_index.core.prompts import PromptTemplate


# 定制回答Prompt
QA_PROMPT = PromptTemplate("""
你是一个严谨的知识库助手。请严格基于下方【参考资料】回答问题。

## 回答规则
1. 只基于参考资料回答,不要使用自身知识
2. 如果资料中没有相关信息,回答"根据现有资料,我无法回答该问题"
3. 不要编造或推断资料中不存在的信息
4. 在关键信息后标注来源[资料X]
5. 回答控制在200-500字

## 参考资料
{context_str}

## 用户问题
{query_str}

## 回答
""")


def build_rag_pipeline(index, nodes):
    """构建完整的RAG Pipeline"""
    
    # 1. 检索器(混合检索)
    retriever = create_hybrid_retriever(index, nodes, top_k=10)
    
    # 2. 后处理器(过滤 + 重排序)
    postprocessors = create_postprocessors()
    postprocessors.append(create_reranker())  # 最后一步重排序
    
    # 3. 回答合成器
    response_synthesizer = get_response_synthesizer(
        response_mode="compact",
        text_qa_template=QA_PROMPT,
    )
    
    # 4. 组装查询引擎
    query_engine = RetrieverQueryEngine(
        retriever=retriever,
        response_synthesizer=response_synthesizer,
        node_postprocessors=postprocessors,
    )
    
    return query_engine


# 完整使用
print("=" * 60)
print("构建RAG Pipeline...")
print("=" * 60)

# Step 1: 文档处理
documents = load_documents("./docs")
nodes = split_documents(documents, strategy="recursive")
nodes = clean_nodes(nodes)

# Step 2: 构建索引
index = create_chroma_index(nodes)

# Step 3: 构建Pipeline
query_engine = build_rag_pipeline(index, nodes)

# Step 4: 提问
questions = [
    "Qwen2.5支持多少种语言?",
    "LoRA和QLoRA有什么区别?",
    "如何减少大模型的幻觉?",
]

for q in questions:
    print(f"\n{'='*60}")
    print(f"问题:{q}")
    print(f"{'='*60}")
    
    response = query_engine.query(q)
    print(f"\n回答:{response}")
    
    print(f"\n来源:")
    for i, node in enumerate(response.source_nodes, 1):
        score = node.score or 0
        source = node.metadata.get("file_name", "未知")
        print(f"  [{i}] score={score:.3f} | {source} | {node.text[:60]}...")

27.4 LangChain 实战:模块化RAG

27.4.1 环境安装

bash 复制代码
pip install langchain langchain-community
pip install langchain-huggingface langchain-openai
pip install langchain-chroma langchain-postgres
pip install rank-bm25 sentence-transformers

27.4.2 LangChain RAG 架构

LangChain的设计哲学是"组合"------每个环节都是独立组件,通过Chain串联:

复制代码
文档加载器 → 文本切分器 → Embedding模型 → 向量数据库
                                                ↓
用户问题 → 查询改写 → 检索器 → 重排序器 → 上下文组装 → LLM → 回答

27.4.3 完整LangChain RAG实现

python 复制代码
from langchain_community.document_loaders import (
    DirectoryLoader,
    TextLoader,
    PyPDFLoader,
    Docx2txtLoader,
    UnstructuredMarkdownLoader,
)
from langchain_text_splitters import (
    RecursiveCharacterTextSplitter,
    MarkdownHeaderTextSplitter,
)
from langchain_huggingface import HuggingFaceEmbeddings
from langchain_chroma import Chroma
from langchain_openai import ChatOpenAI
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.runnables import RunnablePassthrough, RunnableParallel
from langchain_core.output_parsers import StrOutputParser
from langchain_community.retrievers import BM25Retriever
from langchain.retrievers import (
    ContextualCompressionRetriever,
    EnsembleRetriever,
)
from langchain.retrievers.document_compressors import (
    DocumentCompressorPipeline,
    EmbeddingsFilter,
)
import os


# ========================================
# 1. 文档加载
# ========================================
class DocumentLoader:
    """多格式文档加载器"""
    
    LOADERS = {
        ".txt": TextLoader,
        ".pdf": PyPDFLoader,
        ".docx": Docx2txtLoader,
        ".md": UnstructuredMarkdownLoader,
    }
    
    def __init__(self, directory: str):
        self.directory = directory
    
    def load(self) -> list:
        """加载目录下所有支持的文档"""
        from pathlib import Path
        
        documents = []
        for file_path in Path(self.directory).rglob("*"):
            if file_path.suffix.lower() in self.LOADERS:
                loader = self.LOADERS[file_path.suffix.lower()](
                    str(file_path)
                )
                docs = loader.load()
                # 添加文件名作为元数据
                for doc in docs:
                    doc.metadata["source"] = file_path.name
                    doc.metadata["file_type"] = file_path.suffix
                documents.extend(docs)
        
        print(f"加载完成:{len(documents)} 个文档")
        return documents


# ========================================
# 2. 文档切分
# ========================================
class DocumentSplitter:
    """文档切分器"""
    
    @staticmethod
    def split(documents: list, chunk_size: int = 400, overlap: int = 60):
        """递归切分"""
        splitter = RecursiveCharacterTextSplitter(
            chunk_size=chunk_size,
            chunk_overlap=overlap,
            separators=["\n\n", "\n", "。", "!", "?", ".", "!", "?", " ", ""],
        )
        chunks = splitter.split_documents(documents)
        print(f"切分完成:{len(documents)} 文档 → {len(chunks)} 片段")
        return chunks
    
    @staticmethod
    def split_markdown(documents: list, chunk_size: int = 400):
        """Markdown结构切分"""
        headers_to_split_on = [
            ("#", "Header 1"),
            ("##", "Header 2"),
            ("###", "Header 3"),
        ]
        md_splitter = MarkdownHeaderTextSplitter(
            headers_to_split_on=headers_to_split_on
        )
        
        # 先按Markdown结构切
        md_chunks = []
        for doc in documents:
            md_chunks.extend(md_splitter.split_text(doc.page_content))
        
        # 再按长度切
        return DocumentSplitter.split(md_chunks, chunk_size=chunk_size)


# ========================================
# 3. 向量化 + 存储
# ========================================
class VectorStoreManager:
    """向量数据库管理"""
    
    def __init__(self, embedding_model: str = "BAAI/bge-large-zh-v1.5"):
        self.embeddings = HuggingFaceEmbeddings(
            model_name=embedding_model,
            model_kwargs={"device": "cpu"},
            encode_kwargs={"normalize_embeddings": True},
        )
        self.vectorstore = None
    
    def create_from_documents(self, documents: list, persist_dir: str = "./chroma_db"):
        """从文档创建向量数据库"""
        self.vectorstore = Chroma.from_documents(
            documents=documents,
            embedding=self.embeddings,
            persist_directory=persist_dir,
            collection_name="knowledge_base",
        )
        print(f"向量数据库创建完成:{len(documents)} 条记录")
        return self.vectorstore
    
    def load_existing(self, persist_dir: str = "./chroma_db"):
        """加载已有向量数据库"""
        self.vectorstore = Chroma(
            persist_directory=persist_dir,
            embedding_function=self.embeddings,
            collection_name="knowledge_base",
        )
        return self.vectorstore


# ========================================
# 4. 检索器
# ========================================
class HybridRetriever:
    """混合检索器:向量 + BM25"""
    
    def __init__(self, vectorstore, documents, top_k: int = 10):
        self.vector_retriever = vectorstore.as_retriever(
            search_type="similarity_score_threshold",
            search_kwargs={"k": top_k, "score_threshold": 0.3},
        )
        self.bm25_retriever = BM25Retriever.from_documents(documents)
        self.bm25_retriever.k = top_k
        
        # 集成检索器(自动融合)
        self.ensemble_retriever = EnsembleRetriever(
            retrievers=[self.vector_retriever, self.bm25_retriever],
            weights=[0.7, 0.3],  # 向量权重70%,BM25权重30%
        )
    
    def retrieve(self, query: str) -> list:
        """检索"""
        return self.ensemble_retriever.invoke(query)


# ========================================
# 5. 重排序器
# ========================================
class Reranker:
    """重排序器"""
    
    def __init__(self, model_name: str = "BAAI/bge-reranker-v2-m3"):
        from sentence_transformers import CrossEncoder
        self.model = CrossEncoder(model_name)
    
    def rerank(self, query: str, documents: list, top_n: int = 5) -> list:
        """重排序"""
        pairs = [(query, doc.page_content) for doc in documents]
        scores = self.model.predict(pairs)
        
        # 按分数排序
        scored_docs = list(zip(documents, scores))
        scored_docs.sort(key=lambda x: x[1], reverse=True)
        
        # 返回Top-N
        result = []
        for doc, score in scored_docs[:top_n]:
            doc.metadata["rerank_score"] = float(score)
            result.append(doc)
        
        return result


# ========================================
# 6. 上下文组装
# ========================================
def format_context(documents: list) -> str:
    """格式化检索结果为上下文文本"""
    parts = []
    for i, doc in enumerate(documents, 1):
        source = doc.metadata.get("source", "未知")
        content = doc.page_content.strip()
        parts.append(f"【资料{i}】来源:{source}\n{content}")
    return "\n\n---\n\n".join(parts)


# ========================================
# 7. RAG Chain
# ========================================
def build_rag_chain(retriever, reranker, llm):
    """构建RAG Chain"""
    
    # Prompt模板
    prompt = ChatPromptTemplate.from_template("""
你是一个严谨的知识库助手。请严格基于下方【参考资料】回答问题。

## 回答规则
1. 只基于参考资料回答,不要使用自身训练数据中的知识
2. 如果资料中没有相关信息,回答"根据现有资料,我无法回答该问题"
3. 不要编造或推断资料中不存在的信息
4. 在关键信息后标注来源[资料X]
5. 回答控制在200-500字

## 参考资料
{context}

## 用户问题
{question}

## 回答
""")
    
    # 定义Chain
    def retrieve_and_rerank(question):
        """检索 + 重排序"""
        docs = retriever.retrieve(question)
        if reranker:
            docs = reranker.rerank(question, docs, top_n=5)
        return docs
    
    # LCEL Chain(LangChain Expression Language)
    rag_chain = (
        {
            "context": retrieve_and_rerank | format_context,
            "question": RunnablePassthrough(),
        }
        | prompt
        | llm
        | StrOutputParser()
    )
    
    return rag_chain


# ========================================
# 8. 流式输出Chain
# ========================================
def build_streaming_chain(retriever, reranker, llm):
    """流式输出RAG Chain"""
    
    prompt = ChatPromptTemplate.from_template(RAG_PROMPT_TEXT)
    
    def retrieve_and_rerank(question):
        docs = retriever.retrieve(question)
        if reranker:
            docs = reranker.rerank(question, docs, top_n=5)
        return docs
    
    rag_chain = (
        {
            "context": retrieve_and_rerank | format_context,
            "question": RunnablePassthrough(),
        }
        | prompt
        | llm
        | StrOutputParser()
    )
    
    return rag_chain


RAG_PROMPT_TEXT = """
你是一个严谨的知识库助手。请严格基于下方【参考资料】回答问题。

## 回答规则
1. 只基于参考资料回答,不要使用自身知识
2. 如果资料中没有相关信息,回答"根据现有资料,我无法回答该问题"
3. 不要编造或推断资料中不存在的信息
4. 在关键信息后标注来源[资料X]
5. 回答控制在200-500字

## 参考资料
{context}

## 用户问题
{question}

## 回答
"""


# ========================================
# 9. 完整使用
# ========================================
def main():
    """完整RAG系统使用示例"""
    
    # 配置LLM
    llm = ChatOpenAI(
        model="qwen2.5:7b",
        base_url="http://localhost:11434/v1",
        api_key="not-needed",
        temperature=0.1,
        max_tokens=1000,
    )
    
    # Step 1: 加载文档
    loader = DocumentLoader("./docs")
    documents = loader.load()
    
    # Step 2: 切分
    chunks = DocumentSplitter.split(documents, chunk_size=400, overlap=60)
    
    # Step 3: 向量化 + 存储
    vs_manager = VectorStoreManager("BAAI/bge-large-zh-v1.5")
    vectorstore = vs_manager.create_from_documents(chunks)
    
    # Step 4: 检索器
    retriever = HybridRetriever(vectorstore, chunks, top_k=10)
    
    # Step 5: 重排序器
    reranker = Reranker("BAAI/bge-reranker-v2-m3")
    
    # Step 6: 构建Chain
    rag_chain = build_rag_chain(retriever, reranker, llm)
    
    # Step 7: 提问
    question = "Qwen2.5支持多少种语言?"
    print(f"问题:{question}")
    print(f"回答:{rag_chain.invoke(question)}")
    
    # 流式输出
    print("\n--- 流式输出 ---")
    question2 = "LoRA和QLoRA有什么区别?"
    print(f"问题:{question2}")
    for chunk in rag_chain.stream(question2):
        print(chunk, end="", flush=True)
    print()


if __name__ == "__main__":
    main()

27.5 生产级RAG系统架构

27.5.1 系统架构图

复制代码
┌─────────────────────────────────────────────────────────┐
│                      用户层(前端)                        │
│  Web界面 / API接口 / 流式输出 / 反馈收集                   │
└──────────────────────────┬──────────────────────────────┘
                           │
┌──────────────────────────▼──────────────────────────────┐
│                    API网关层(FastAPI)                    │
│  请求路由 / 鉴权 / 限流 / 日志 / 链路追踪                   │
└──────────────────────────┬──────────────────────────────┘
                           │
┌──────────────────────────▼──────────────────────────────┐
│                    RAG服务层(核心)                       │
│                                                          │
│  ┌──────────┐  ┌──────────┐  ┌──────────┐  ┌─────────┐ │
│  │ 查询改写  │→│ 混合检索  │→│ 重排序   │→│ 生成优化 │ │
│  │ (LLM)    │  │(向量+BM25)│  │(BGE)    │  │(Prompt) │ │
│  └──────────┘  └──────────┘  └──────────┘  └─────────┘ │
│                                                      │   │
│  ┌──────────┐  ┌──────────┐  ┌──────────┐            │
│  │ 幻觉检测  │  │ 引用标注  │  │ 缓存层   │            │
│  │ (LLM自检) │  │ (后处理)  │  │(Redis)  │            │
│  └──────────┘  └──────────┘  └──────────┘            │
└──────────┬───────────────┬───────────────┬────────────┘
           │               │               │
┌──────────▼─────┐ ┌──────▼───────┐ ┌────▼──────────┐
│   向量数据库    │ │  关键词索引   │ │   LLM服务      │
│  (Milvus/PG)   │ │  (Elasticsearch)│ (vLLM/Ollama) │
└────────────────┘ └──────────────┘ └───────────────┘
           │
┌──────────▼─────────────────────────────────────────────┐
│                  数据处理层(离线)                       │
│  文档加载 → 清洗 → 切分 → 向量化 → 入库(定时/手动触发)   │
└─────────────────────────────────────────────────────────┘

27.5.2 FastAPI 服务封装

python 复制代码
from fastapi import FastAPI, HTTPException
from fastapi.responses import StreamingResponse
from pydantic import BaseModel
from typing import Optional
import uuid
import time

app = FastAPI(title="RAG知识库API", version="1.0")


# ===== 数据模型 =====
class Question(BaseModel):
    question: str
    top_k: Optional[int] = 5
    use_rerank: Optional[bool] = True
    stream: Optional[bool] = False


class Answer(BaseModel):
    answer: str
    sources: list
    latency_ms: int
    request_id: str


# ===== 全局RAG引擎 =====
class RAGEngine:
    """RAG引擎单例"""
    
    def __init__(self):
        self.query_engine = None
        self.retriever = None
        self.reranker = None
        self.llm = None
        self._initialized = False
    
    def initialize(self):
        """初始化(加载模型、连接数据库)"""
        print("初始化RAG引擎...")
        
        # 加载LLM
        self.llm = ChatOpenAI(
            model="qwen2.5:7b",
            base_url="http://localhost:11434/v1",
            api_key="not-needed",
            temperature=0.1,
        )
        
        # 加载Embedding
        self.embeddings = HuggingFaceEmbeddings(
            model_name="BAAI/bge-large-zh-v1.5",
            model_kwargs={"device": "cpu"},
        )
        
        # 加载向量数据库
        self.vectorstore = Chroma(
            persist_directory="./chroma_db",
            embedding_function=self.embeddings,
            collection_name="knowledge_base",
        )
        
        # 检索器
        self.retriever = HybridRetriever(
            self.vectorstore, [], top_k=10
        )
        
        # 重排序器
        self.reranker = Reranker("BAAI/bge-reranker-v2-m3")
        
        # 构建Chain
        self.rag_chain = build_rag_chain(
            self.retriever, self.reranker, self.llm
        )
        
        self._initialized = True
        print("RAG引擎初始化完成")
    
    def query(self, question: str, top_k: int = 5) -> dict:
        """查询"""
        if not self._initialized:
            raise RuntimeError("RAG引擎未初始化")
        
        start_time = time.time()
        request_id = str(uuid.uuid4())[:8]
        
        # 检索
        docs = self.retriever.retrieve(question)
        if self.reranker:
            docs = self.reranker.rerank(question, docs, top_n=top_k)
        
        # 生成
        context = format_context(docs)
        answer = self.rag_chain.invoke(question)
        
        latency = int((time.time() - start_time) * 1000)
        
        return {
            "answer": answer,
            "sources": [
                {
                    "source": d.metadata.get("source", "未知"),
                    "score": d.metadata.get("rerank_score", 0),
                    "preview": d.page_content[:100],
                }
                for d in docs
            ],
            "latency_ms": latency,
            "request_id": request_id,
        }
    
    def query_stream(self, question: str, top_k: int = 5):
        """流式查询"""
        docs = self.retriever.retrieve(question)
        if self.reranker:
            docs = self.reranker.rerank(question, docs, top_n=top_k)
        
        for chunk in self.rag_chain.stream(question):
            yield chunk


# 全局实例
rag_engine = RAGEngine()


@app.on_event("startup")
async def startup():
    """启动时初始化"""
    rag_engine.initialize()


@app.post("/api/v1/ask", response_model=Answer)
async def ask(q: Question):
    """问答接口"""
    try:
        result = rag_engine.query(q.question, q.top_k)
        return Answer(**result)
    except Exception as e:
        raise HTTPException(status_code=500, detail=str(e))


@app.post("/api/v1/ask/stream")
async def ask_stream(q: Question):
    """流式问答接口"""
    async def generate():
        for chunk in rag_engine.query_stream(q.question, q.top_k):
            yield f"data: {chunk}\n\n"
    
    return StreamingResponse(
        generate(),
        media_type="text/event-stream"
    )


@app.get("/api/v1/health")
async def health():
    """健康检查"""
    return {
        "status": "healthy" if rag_engine._initialized else "initializing",
        "model": "qwen2.5:7b",
        "embedding": "bge-large-zh-v1.5",
    }


# 启动命令:uvicorn api:app --host 0.0.0.0 --port 8000

27.5.3 缓存层设计

python 复制代码
import hashlib
import json
import redis


class ResponseCache:
    """响应缓存 - 相同问题直接返回缓存结果"""
    
    def __init__(self, redis_url: str = "redis://localhost:6379"):
        self.redis = redis.from_url(redis_url)
        self.ttl = 3600  # 缓存1小时
    
    def _make_key(self, question: str, top_k: int) -> str:
        """生成缓存Key"""
        content = f"{question}:{top_k}"
        return f"rag:answer:{hashlib.md5(content.encode()).hexdigest()}"
    
    def get(self, question: str, top_k: int = 5) -> Optional[dict]:
        """获取缓存"""
        key = self._make_key(question, top_k)
        cached = self.redis.get(key)
        if cached:
            result = json.loads(cached)
            result["from_cache"] = True
            return result
        return None
    
    def set(self, question: str, top_k: int, result: dict):
        """设置缓存"""
        key = self._make_key(question, top_k)
        # 不缓存流式响应
        self.redis.setex(key, self.ttl, json.dumps(result, ensure_ascii=False))


# 在RAGEngine中加入缓存
class CachedRAGEngine(RAGEngine):
    def __init__(self):
        super().__init__()
        self.cache = ResponseCache()
    
    def query(self, question: str, top_k: int = 5) -> dict:
        # 先查缓存
        cached = self.cache.get(question, top_k)
        if cached:
            print(f"缓存命中: {question}")
            return cached
        
        # 缓存未命中,正常查询
        result = super().query(question, top_k)
        
        # 写入缓存
        self.cache.set(question, top_k, result)
        
        return result

27.6 文档管理后台

27.6.1 文档增删改查

python 复制代码
class DocumentManager:
    """文档管理器 - 支持增量更新"""
    
    def __init__(self, vectorstore, embeddings):
        self.vectorstore = vectorstore
        self.embeddings = embeddings
    
    def add_document(self, file_path: str, metadata: dict = None):
        """添加单个文档"""
        loader = DocumentLoader("")
        ext = os.path.splitext(file_path)[1].lower()
        
        if ext not in loader.LOADERS:
            raise ValueError(f"不支持的格式: {ext}")
        
        loader_cls = loader.LOADERS[ext]
        docs = loader_cls(file_path).load()
        
        if metadata:
            for doc in docs:
                doc.metadata.update(metadata)
        
        # 切分
        chunks = DocumentSplitter.split(docs, chunk_size=400, overlap=60)
        
        # 入库
        ids = self.vectorstore.add_documents(chunks)
        print(f"添加成功:{file_path} → {len(ids)} 个片段")
        return ids
    
    def delete_document(self, file_name: str):
        """删除文档(按文件名)"""
        # Chroma支持按元数据过滤删除
        self.vectorstore.delete(
            where={"source": file_name}
        )
        print(f"删除成功:{file_name}")
    
    def update_document(self, file_path: str):
        """更新文档(先删后加)"""
        file_name = os.path.basename(file_path)
        self.delete_document(file_name)
        self.add_document(file_path)
        print(f"更新成功:{file_path}")
    
    def list_documents(self) -> list:
        """列出所有文档"""
        # 从Chroma获取所有文档的元数据
        results = self.vectorstore.get()
        
        # 按文件名去重
        sources = set()
        for metadata in results["metadatas"]:
            sources.add(metadata.get("source", "未知"))
        
        return sorted(sources)
    
    def get_stats(self) -> dict:
        """获取统计信息"""
        results = self.vectorstore.get()
        return {
            "total_chunks": len(results["ids"]),
            "total_documents": len(set(
                m.get("source", "未知") for m in results["metadatas"]
            )),
            "collection_name": "knowledge_base",
        }

27.6.2 文档管理API

python 复制代码
from fastapi import UploadFile, File

@app.post("/api/v1/documents/upload")
async def upload_document(file: UploadFile = File(...)):
    """上传文档"""
    # 保存文件
    file_path = f"./docs/{file.filename}"
    with open(file_path, "wb") as f:
        f.write(await file.read())
    
    # 添加到向量数据库
    rag_engine.doc_manager.add_document(file_path)
    
    return {"status": "success", "filename": file.filename}


@app.delete("/api/v1/documents/{filename}")
async def delete_document(filename: str):
    """删除文档"""
    rag_engine.doc_manager.delete_document(filename)
    return {"status": "success", "deleted": filename}


@app.get("/api/v1/documents")
async def list_documents():
    """列出所有文档"""
    docs = rag_engine.doc_manager.list_documents()
    stats = rag_engine.doc_manager.get_stats()
    return {"documents": docs, "stats": stats}


@app.post("/api/v1/documents/rebuild")
async def rebuild_index():
    """重建索引"""
    # 清空现有索引
    # 重新加载所有文档
    # 重新切分和向量化
    return {"status": "rebuilding", "message": "索引重建中..."}

27.7 Spring AI 集成(Java版)

对于后端团队来说,可能更习惯用Java。Spring AI提供了与LangChain类似的抽象:

27.7.1 Maven依赖

xml 复制代码
<dependencies>
    <!-- Spring AI核心 -->
    <dependency>
        <groupId>org.springframework.ai</groupId>
        <artifactId>spring-ai-openai-spring-boot-starter</artifactId>
    </dependency>
    
    <!-- 向量数据库 -->
    <dependency>
        <groupId>org.springframework.ai</groupId>
        <artifactId>spring-ai-pgvector-store-spring-boot-starter</artifactId>
    </dependency>
    
    <!-- 文档加载 -->
    <dependency>
        <groupId>org.springframework.ai</groupId>
        <artifactId>spring-ai-tika-document-reader</artifactId>
    </dependency>
</dependencies>

27.7.2 配置

yaml 复制代码
# application.yml
spring:
  ai:
    openai:
      base-url: http://localhost:11434
      api-key: not-needed
      chat:
        options:
          model: qwen2.5:7b
          temperature: 0.1
      embedding:
        options:
          model: bge-large-zh-v1.5
    vectorstore:
      pgvector:
        dimensions: 1024
        distance-type: cosine_distance
        schema-name: public
        table-name: knowledge_base

27.7.3 RAG服务实现

java 复制代码
@Service
public class RagService {
    
    @Autowired
    private ChatClient chatClient;
    
    @Autowired
    private VectorStore vectorStore;
    
    private static final String RAG_PROMPT = """
        你是一个严谨的知识库助手。请严格基于下方【参考资料】回答问题。
        
        ## 回答规则
        1. 只基于参考资料回答,不要使用自身知识
        2. 如果资料中没有相关信息,回答"根据现有资料,我无法回答该问题"
        3. 不要编造或推断资料中不存在的信息
        4. 在关键信息后标注来源[资料X]
        5. 回答控制在200-500字
        
        ## 参考资料
        {context}
        
        ## 用户问题
        {question}
        
        ## 回答
        """;
    
    /**
     * RAG问答
     */
    public String ask(String question) {
        // 1. 检索相关文档
        List<Document> documents = vectorStore.similaritySearch(
            SearchRequest.query(question)
                .withTopK(5)
                .withSimilarityThreshold(0.5)
        );
        
        // 2. 组装上下文
        String context = formatContext(documents);
        
        // 3. 生成回答
        return chatClient.prompt()
            .user(u -> u.text(RAG_PROMPT)
                .param("context", context)
                .param("question", question))
            .call()
            .content();
    }
    
    /**
     * 流式问答
     */
    public Flux<String> askStream(String question) {
        List<Document> documents = vectorStore.similaritySearch(
            SearchRequest.query(question).withTopK(5)
        );
        String context = formatContext(documents);
        
        return chatClient.prompt()
            .user(u -> u.text(RAG_PROMPT)
                .param("context", context)
                .param("question", question))
            .stream()
            .content();
    }
    
    /**
     * 添加文档到知识库
     */
    public void addDocument(Resource resource, String source) {
        TikaDocumentReader reader = new TikaDocumentReader(resource);
        List<Document> documents = reader.get();
        
        // 切分
        TokenTextSplitter splitter = new TokenTextSplitter();
        List<Document> chunks = splitter.apply(documents);
        
        // 添加元数据
        chunks.forEach(doc -> {
            doc.getMetadata().put("source", source);
        });
        
        // 入库
        vectorStore.add(chunks);
    }
    
    private String formatContext(List<Document> documents) {
        StringBuilder sb = new StringBuilder();
        for (int i = 0; i < documents.size(); i++) {
            Document doc = documents.get(i);
            String source = (String) doc.getMetadata().getOrDefault("source", "未知");
            sb.append(String.format("【资料%d】来源:%s\n%s\n\n---\n\n",
                i + 1, source, doc.getText()));
        }
        return sb.toString();
    }
}

27.7.4 Controller

java 复制代码
@RestController
@RequestMapping("/api/v1")
public class RagController {
    
    @Autowired
    private RagService ragService;
    
    @PostMapping("/ask")
    public ResponseEntity<Map<String, Object>> ask(@RequestBody Map<String, String> body) {
        String question = body.get("question");
        String answer = ragService.ask(question);
        return ResponseEntity.ok(Map.of(
            "answer", answer,
            "status", "success"
        ));
    }
    
    @PostMapping(value = "/ask/stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
    public Flux<String> askStream(@RequestBody Map<String, String> body) {
        String question = body.get("question");
        return ragService.askStream(question);
    }
    
    @PostMapping("/documents/upload")
    public ResponseEntity<?> upload(@RequestParam("file") MultipartFile file) {
        try {
            Resource resource = file.getResource();
            ragService.addDocument(resource, file.getOriginalFilename());
            return ResponseEntity.ok(Map.of("status", "success"));
        } catch (Exception e) {
            return ResponseEntity.badRequest().body(Map.of("error", e.getMessage()));
        }
    }
}

27.8 RAG系统评估与调优

27.8.1 构建评估Pipeline

python 复制代码
class RAGEvaluator:
    """RAG系统评估器"""
    
    def __init__(self, rag_chain, retriever, reranker):
        self.rag_chain = rag_chain
        self.retriever = retriever
        self.reranker = reranker
    
    def evaluate(self, test_cases: list) -> dict:
        """
        test_cases: [
            {
                "question": "Qwen2.5支持多少种语言?",
                "ground_truth": "Qwen2.5支持35种语言",
                "expected_source": "qwen_tech.md"
            },
            ...
        ]
        """
        results = []
        
        for case in test_cases:
            question = case["question"]
            ground_truth = case["ground_truth"]
            
            # 检索评估
            docs = self.retriever.retrieve(question)
            if self.reranker:
                docs = self.reranker.rerank(question, docs, top_n=5)
            
            retrieval_hit = any(
                case.get("expected_source", "") in d.metadata.get("source", "")
                for d in docs
            )
            
            # 生成评估
            answer = self.rag_chain.invoke(question)
            
            # 忠实度评估
            faithfulness = self._eval_faithfulness(answer, docs)
            
            # 回答正确性
            correctness = self._eval_correctness(answer, ground_truth)
            
            results.append({
                "question": question,
                "answer": answer,
                "ground_truth": ground_truth,
                "retrieval_hit": retrieval_hit,
                "faithfulness": faithfulness,
                "correctness": correctness,
            })
        
        # 汇总
        n = len(results)
        return {
            "results": results,
            "summary": {
                "retrieval_hit_rate": sum(r["retrieval_hit"] for r in results) / n,
                "avg_faithfulness": sum(r["faithfulness"] for r in results) / n,
                "avg_correctness": sum(r["correctness"] for r in results) / n,
                "total_cases": n,
            }
        }
    
    def _eval_faithfulness(self, answer: str, docs: list) -> float:
        """评估忠实度"""
        context = format_context(docs)
        # 简化版:检查回答中的数字是否在上下文中
        import re
        numbers = re.findall(r'\d+\.?\d*', answer)
        if not numbers:
            return 1.0
        covered = sum(1 for n in numbers if n in context)
        return covered / len(numbers)
    
    def _eval_correctness(self, answer: str, ground_truth: str) -> float:
        """评估正确性(与标准答案的相似度)"""
        from difflib import SequenceMatcher
        return SequenceMatcher(None, answer, ground_truth).ratio()


# 使用
evaluator = RAGEvaluator(rag_chain, retriever, reranker)

test_cases = [
    {
        "question": "Qwen2.5支持多少种语言?",
        "ground_truth": "Qwen2.5支持35种语言",
        "expected_source": "qwen_tech.md"
    },
    {
        "question": "LoRA和全量微调有什么区别?",
        "ground_truth": "LoRA只训练少量参数,全量微调更新全部参数",
        "expected_source": "finetune.md"
    },
]

report = evaluator.evaluate(test_cases)
print(f"检索命中率: {report['summary']['retrieval_hit_rate']:.2%}")
print(f"平均忠实度: {report['summary']['avg_faithfulness']:.2%}")
print(f"平均正确性: {report['summary']['avg_correctness']:.2%}")

27.8.2 调优决策表

评估指标低 可能原因 调优方向
检索命中率低 切分太粗/Embedding不好/没有混合检索 调chunk_size、换Embedding模型、加BM25
忠实度低 温度太高/Prompt约束不够/上下文太多 降温度、加强Prompt、减少top_k
正确性低 检索到但不相关/Prompt不好 加重排序、优化Prompt
引用准确率低 模型标注能力弱 后处理修正引用
延迟高 检索慢/重排序慢/生成慢 加缓存、用更小模型、异步处理

27.9 从Demo到生产:关键差异

维度 Demo 生产
文档量 10-100个 1万-100万
并发 单用户 100-1000 QPS
可用性 跑通就行 99.9% SLA
监控 没有 全链路追踪
文档更新 手动 自动Pipeline
缓存 没有 Redis多级缓存
降级 没有 模型降级+兜底回复
安全 没有 Prompt注入防护+鉴权
评估 肉眼看 自动化评估Pipeline
成本 不关心 Token监控+模型分级路由

生产级优化清单

python 复制代码
# ===== 1. 异步处理 =====
@app.post("/api/v1/ask/async")
async def ask_async(q: Question, background_tasks: BackgroundTasks):
    """异步问答 - 适合长查询"""
    task_id = str(uuid.uuid4())
    # 放入任务队列,异步处理
    background_tasks.add_task(process_async, task_id, q.question)
    return {"task_id": task_id, "status": "processing"}


# ===== 2. 模型降级 =====
class ModelRouter:
    """模型分级路由"""
    
    def __init__(self):
        self.models = {
            "simple": "qwen2.5:1.5b",   # 简单问题用小模型
            "standard": "qwen2.5:7b",   # 标准问题用中模型
            "complex": "qwen2.5:14b",   # 复杂问题用大模型
        }
    
    def route(self, question: str) -> str:
        """根据问题复杂度选择模型"""
        if len(question) < 20:
            return self.models["simple"]
        elif len(question) < 100:
            return self.models["standard"]
        else:
            return self.models["complex"]


# ===== 3. 限流 =====
from slowapi import Limiter
from slowapi.util import get_remote_address

limiter = Limiter(key_func=get_remote_address)

@app.post("/api/v1/ask")
@limiter.limit("10/minute")  # 每分钟10次
async def ask(request: Request, q: Question):
    result = rag_engine.query(q.question)
    return result


# ===== 4. 链路追踪 =====
from loguru import logger

@app.middleware("http")
async def log_requests(request: Request, call_next):
    """请求日志中间件"""
    request_id = str(uuid.uuid4())[:8]
    start = time.time()
    
    logger.info(f"[{request_id}] {request.method} {request.url.path}")
    
    response = await call_next(request)
    
    duration = time.time() - start
    logger.info(f"[{request_id}] completed in {duration:.3f}s")
    
    response.headers["X-Request-ID"] = request_id
    return response

27.10 常见问题与最佳实践

27.10.1 常见问题

问题 原因 解决方案
内存溢出 文档太多一次性加载 分批处理、流式加载
检索慢 向量数据库索引没建 建HNSW索引、增加ef_construction
回答重复 上下文有重复片段 加强去重、提高dedup阈值
中文切分差 按英文逻辑切分 用中文分隔符、加句号断句
模型不引用 Prompt约束不够 强化Prompt、加引用后处理
多轮对话跑偏 没做查询改写 加查询改写模块

27.10.2 最佳实践清单

数据处理:

  • 多格式文档统一加载
  • 数据清洗Pipeline(去噪/去重/规范化)
  • 递归切分 chunk_size=400, overlap=60 起步
  • 元数据自动标注(source/date/category)
  • 增量更新支持

检索:

  • 混合检索(向量+BM25)
  • 重排序(BGE Reranker)
  • 检索结果去重
  • 元数据过滤

生成:

  • 低温度(0.1-0.3)
  • Prompt明确约束(只基于资料)
  • 引用标注
  • 流式输出
  • 幻觉检测

工程化:

  • FastAPI服务封装
  • Redis缓存
  • 模型分级路由
  • 限流+鉴权
  • 链路追踪
  • 文档管理后台
  • 评估Pipeline
  • 监控告警

27.11 本章小结

核心知识点回顾

复制代码
RAG系统完整架构:

离线Pipeline:文档加载 → 清洗 → 切分 → 向量化 → 入库
在线Pipeline:查询改写 → 混合检索 → 重排序 → 上下文组装 → Prompt → 生成 → 引用标注 → 幻觉检测

框架选择:
- LlamaIndex:纯RAG项目首选,上手快,文档处理好
- LangChain:Agent+RAG混合项目,生态大,扩展强
- Spring AI:Java后端团队首选,企业级集成

生产化关键:
- 缓存(Redis)、降级(模型路由)、监控(链路追踪)
- 评估Pipeline(检索命中率+忠实度+正确性)
- 文档管理后台(增删改查+增量更新)

关键结论

  1. 框架是工具不是目的 --- 先理解RAG原理(第21-26章),再用框架提效。不懂原理用框架只是"黑盒魔法"
  2. LlamaIndex上手最快 --- 30行代码跑通RAG,适合快速验证
  3. LangChain扩展性最强 --- LCEL链式组合灵活,适合复杂应用
  4. Spring AI是Java团队的最佳选择 --- 与Spring生态无缝集成
  5. 从Demo到生产差距很大 --- 缓存、降级、监控、评估、文档管理,这些才是生产级系统的核心
  6. 评估是持续过程 --- 没有评估就没有优化,建立测试集 + 自动化评估Pipeline

第5阶段「RAG检索增强生成」完整回顾

章节 核心知识
第21章 RAG原理 检索→增强→生成、RAG vs 微调、三代演进
第22章 数据处理 文档加载、清洗、五种切分策略、元数据管理
第23章 向量化 Embedding原理、BGE/M3E选型、维度选择
第24章 向量数据库 HNSW/IVF/PQ索引、Milvus/PgVector/Chroma选型
第25章 检索策略 查询改写、混合检索(RRF)、重排序(BGE Reranker)
第26章 生成优化 上下文组装、Prompt设计、引用标注、幻觉检测
第27章 框架实操 LlamaIndex/LangChain/Spring AI完整实战

总进度

阶段 章节 状态
第1阶段 认知启蒙 第1-4章 ✅ 全部完成
第2阶段 机器学习基础 第5-9章 ✅ 全部完成
第3阶段 深度学习 第10-14章 ✅ 全部完成
第4阶段 大模型(LLM) 第15-20章 ✅ 全部完成
第5阶段 RAG检索增强生成 第21-27章 ✅ 全部完成
第6阶段 Agent智能体 第28章起 待写

大白话总结: 至此第5阶段「RAG检索增强生成」7章全部完成。RAG是目前AI应用开发中落地最多、ROI最高的场景------不需要训练模型,只需要"检索+生成"就能让大模型用上你的私有数据。掌握这7章的内容,你就能搭建一个生产级RAG系统了。

接下来进入第6阶段「Agent智能体」,第28章「Agent概念」会讲清楚AI能不能自己决定"先做什么后做什么"。需要继续写就告诉我。

相关推荐
️学习的小王1 小时前
智能文档助手:基于RAG的本地化文档问答系统实战指南
人工智能·python·机器学习
zhangfeng11331 小时前
CodeBuddy 是否支持 SDD(Spec-Driven Development 规范驱动开发
人工智能·驱动开发
阿里云大数据AI技术1 小时前
基于阿里云EMR Serverless StarRocks提效多模态工单标注和舆情研判
人工智能
等一朵映山红2 小时前
基于 OpenCV 实现摄像头实时人脸检测完整流程
人工智能·opencv·计算机视觉
小兔子2 小时前
Agent 一旦能出网、改仓、调工具:对照 AISI 越权事件,把四层控制写进架构
人工智能·agent
jkyy20142 小时前
以科技赋能运动康养!健康有益×泰康养老,打造智能运动新体系
大数据·人工智能·健康医疗
不瘦80斤不改名2 小时前
05-vibe-coding-向agentic-engineering演进
人工智能·笔记·python·prompt
Smoothcloud润云2 小时前
GPU租赁数据安全怎么做?
人工智能·算法·ai·aigc·gpu算力·gpu
北墨NoLimit2 小时前
TRAE Work实战:把办公Agent竞品调研从2-3天压到32分钟,有完整指令模板
前端·人工智能·数据可视化