AI大模型RAG实战经验分享

【AI大模型应用开发】3.2 RAG实战 - RAG应用+UI实现加载本地文件并对话 前面我们实现了RAG基本流程,今天我们在此基础上给它加个UI界面,在浏览器打开,实现上传本地文件、解析,并用大模型实现与该文档的对话。也就是一个阉割版demo版的ChatPDF或知识库对话系统。 阅读详情

一、RAG到底在解决什么问题

在动手之前,我想先聊聊RAG这个概念,因为很多刚接触的朋友容易搞混。

大模型很强,但它有两个致命弱点:

第一,知识有截止日期。 GPT-4的训练数据截止到某个时间点,它不知道你们公司上周发布的新规范,也不知道你们昨天刚修复的那个bug是怎么解决的。

第二,会一本正经地胡说八道。 当大模型遇到它不知道的问题时,它不会老老实实说我不知道,而是会基于它学过的通用知识,给你编一个看起来很合理但其实是错的答案。这就是所谓的幻觉(Hallucination)。

RAG(Retrieval-Augmented Generation,检索增强生成)的核心思路其实很简单:别让大模型靠想象力答题,先帮它把参考资料找出来,让它照着资料回答。

具体来说分三步:

  1. 把你的私有文档切成小块,转成向量存起来
  2. 用户提问时,先根据问题检索出最相关的文档片段
  3. 把问题和检索到的内容一起喂给大模型,让它基于这些材料生成答案

听起来不复杂对吧?我当时也是这么想的,然后就踩了一堆坑。

二、第一个大坑:文档切分没那么简单

我最初的方案特别粗暴——用LangChain的RecursiveCharacterTextSplitter,设置chunk_size=500,overlap=50,直接把所有文档切成小块。

代码写起来确实很简单:

from langchain.text_splitter import RecursiveCharacterTextSplitterdefnaive_split(text):    最初的简单切分方案——后来证明这是个坑    splitter = RecursiveCharacterTextSplitter(        chunk_size=500,        chunk_overlap=50,        separators=[\n\n, \n, 。, !, ?,  , ]    )    chunks = splitter.split_text(text)return chunks# 测试一下sample_text = # MySQL主从切换操作手册## 1. 前置检查在执行主从切换之前,必须完成以下检查:- 确认从库同步状态正常(Seconds_Behind_Master = 0)- 确认没有正在执行的大事务- 通知相关业务方,确认切换时间窗口## 2. 切换步骤2.1 在主库执行只读设置SET GLOBAL read_only = 1;2.2 等待从库完全同步在从库执行 SHOW SLAVE STATUS,确认 Seconds_Behind_Master = 02.3 停止从库复制STOP SLAVE;RESET SLAVE ALL;## 3. 回滚方案如果切换失败,按以下步骤回滚...chunks = naive_split(sample_text)for i, chunk inenumerate(chunks):print(f Chunk {i+1} )print(chunk[:100] + ... iflen(chunk) > 100else chunk)

看起来没毛病是吧?但实际用起来问题大了。

有一次用户问:MySQL切换前需要做哪些检查?系统返回的文档片段是这样的:

确认没有正在执行的大事务- 通知相关业务方,确认切换时间窗口## 2. 切换步骤2.1 在主库执行只读设置SET GLOBAL read_only = 1;

发现问题了吗?这个片段恰好从检查步骤的中间切开了!第一条检查项确认从库同步状态正常被切到了上一个chunk里。用户问的是需要做哪些检查,结果我们给大模型的参考资料里,第一条检查项就没包含进去。

核心教训:机械地按字数切分,会打断文档的语义完整性。

后来我改成了基于语义结构的切分策略:

import refrom typing importList, DictclassSmartDocumentSplitter:    语义感知的文档切分器    核心思路:尊重文档的原有结构,按标题、段落等语义边界切分def__init__(self, max_chunk_size=800, min_chunk_size=100):        self.max_chunk_size = max_chunk_size        self.min_chunk_size = min_chunk_sizedefsplit_markdown(self, text: str) -> List[Dict]:        针对Markdown文档的切分        保持标题层级结构,每个chunk都带上完整的上下文路径        chunks = []        current_headers = {1: , 2: , 3: }  # 记录当前的标题层级# 按行处理,识别标题和内容        lines = text.split('\n')        current_content = []for line in lines:# 检测Markdown标题            header_match = re.match(r'^(#{1,3})\s+(.+)$', line)if header_match:# 遇到新标题,先保存之前的内容if current_content:                    chunk_text = '\n'.join(current_content).strip()iflen(chunk_text) >= self.min_chunk_size:                        chunks.append({'content': chunk_text,'headers': dict(current_headers),'context_path': self._build_context_path(current_headers)                        })                    current_content = []# 更新标题层级                level = len(header_match.group(1))                title = header_match.group(2)                current_headers[level] = title# 清除下级标题for l inrange(level + 1, 4):                    current_headers[l] =                 current_content.append(line)else:                current_content.append(line)# 如果当前内容超过最大长度,强制切分(但尽量在段落边界)                content_so_far = '\n'.join(current_content)iflen(content_so_far) > self.max_chunk_size:                    chunk_text = content_so_far.strip()                    chunks.append({'content': chunk_text,'headers': dict(current_headers),'context_path': self._build_context_path(current_headers)                    })                    current_content = []# 别忘了最后一段if current_content:            chunk_text = '\n'.join(current_content).strip()iflen(chunk_text) >= self.min_chunk_size:                chunks.append({'content': chunk_text,'headers': dict(current_headers),'context_path': self._build_context_path(current_headers)                })return chunksdef_build_context_path(self, headers: Dict) -> str:        构建层级路径,比如:MySQL主从切换 > 前置检查        path_parts = [h for h in [headers[1], headers[2], headers[3]] if h]return' > '.join(path_parts) if path_parts else'未分类'defenrich_chunk_with_context(self, chunk: Dict) -> str:        关键技巧:给每个chunk加上上下文前缀        这样即使单独看这个片段,也能知道它属于哪个章节        context = f[文档路径:{chunk['context_path']}]\n\nreturn context + chunk['content']# 实际使用示例splitter = SmartDocumentSplitter(max_chunk_size=800)chunks = splitter.split_markdown(sample_text)print(f切分后共 {len(chunks)} 个片段\n)for i, chunk inenumerate(chunks):print(f=== Chunk {i+1} ===)print(f路径:{chunk['context_path']})print(f内容预览:{chunk['content'][:150]}...)print()

这样切出来的效果就好多了。每个chunk开头都会带上它的位置信息,大模型在回答时能更准确地理解这段内容的上下文。

不过说实话,这个方案也不是万能的。对于那些格式不规范的老文档(没有清晰的标题结构),切分效果依然一般。后来我又针对不同类型的文档做了差异化处理,这个我们后面再说。

三、第二个大坑:向量检索的语义鸿沟

解决了切分问题,下一步就是向量化和检索了。我用的是开源的BGE模型做Embedding,用Milvus做向量数据库。

第一版的检索代码很直白:

from sentence_transformers import SentenceTransformerfrom pymilvus import connections, Collection, FieldSchema, CollectionSchema, DataType, utilityimport numpy as npclassVectorStore:    向量存储和检索def__init__(self, model_name='BAAI/bge-base-zh-v1.5'):# 加载Embedding模型        self.model = SentenceTransformer(model_name)        self.dim = 768# BGE base模型的向量维度# 连接Milvus        connections.connect(default, host=localhost, port=19530)defcreate_collection(self, collection_name: str):        创建集合if utility.has_collection(collection_name):            utility.drop_collection(collection_name)        fields = [            FieldSchema(name=id, dtype=DataType.INT64, is_primary=True, auto_id=True),            FieldSchema(name=content, dtype=DataType.VARCHAR, max_length=4096),            FieldSchema(name=context_path, dtype=DataType.VARCHAR, max_length=512),            FieldSchema(name=embedding, dtype=DataType.FLOAT_VECTOR, dim=self.dim)        ]        schema = CollectionSchema(fields, description=知识库文档)        collection = Collection(collection_name, schema)# 创建索引        index_params = {            metric_type: COSINE,            index_type: IVF_FLAT,            params: {nlist: 128}        }        collection.create_index(embedding, index_params)return collectiondefinsert_documents(self, collection_name: str, chunks: list):        插入文档        collection = Collection(collection_name)        contents = [chunk['content'] for chunk in chunks]        context_paths = [chunk['context_path'] for chunk in chunks]# 批量生成Embedding        embeddings = self.model.encode(contents, normalize_embeddings=True)        collection.insert([contents, context_paths, embeddings.tolist()])        collection.flush()print(f成功插入 {len(chunks)} 条文档)defsearch(self, collection_name: str, query: str, top_k: int = 5):        基础检索        collection = Collection(collection_name)        collection.load()# 生成查询向量        query_embedding = self.model.encode([query], normalize_embeddings=True)        results = collection.search(            data=query_embedding.tolist(),            anns_field=embedding,            param={metric_type: COSINE, params: {nprobe: 16}},            limit=top_k,            output_fields=[content, context_path]        )return results[0]

基本功能是没问题的。但实际跑起来,我发现了一个让人抓狂的现象——用户的口语化提问和文档的正式表述之间存在巨大的语义鸿沟。

举个例子:

  • 用户问:数据库挂了怎么办
  • 文档标题是:MySQL服务异常恢复操作手册

这两个在语义上是相关的,但向量相似度可能并不高。因为用户说的挂了和文档里的异常,用词差异很大。

更坑的是,有时候检索出的Top 5结果里,真正相关的那篇可能只排在第3或第4位,但前两名是一些看起来相关但实际上文不对题的内容。如果我只取Top 3喂给大模型,可能就漏掉了最关键的信息。

后来我采用了一个两阶段检索的策略:先用向量检索做粗筛,再用重排序模型做精排。

from transformers import AutoModelForSequenceClassification, AutoTokenizerimport torchclassEnhancedRetriever:    增强版检索器:向量检索 + 重排序def__init__(self, vector_store: VectorStore):        self.vector_store = vector_store# 加载重排序模型(BGE Reranker效果不错)        self.reranker_tokenizer = AutoTokenizer.from_pretrained('BAAI/bge-reranker-base')        self.reranker_model = AutoModelForSequenceClassification.from_pretrained('BAAI/bge-reranker-base')        self.reranker_model.eval()defretrieve_with_rerank(self, collection_name: str, query: str,                              initial_top_k: int = 20, final_top_k: int = 5):        两阶段检索:1. 向量检索召回 initial_top_k 个候选2. 用重排序模型精排,返回 final_top_k 个结果# 第一阶段:向量检索(召回更多候选)        initial_results = self.vector_store.search(collection_name, query, top_k=initial_top_k)ifnot initial_results:return []# 准备重排序        candidates = []for hit in initial_results:            candidates.append({'content': hit.entity.get('content'),'context_path': hit.entity.get('context_path'),'vector_score': hit.score  # 保留向量检索得分,用于调试            })# 第二阶段:重排序        rerank_scores = self._compute_rerank_scores(query, [c['content'] for c in candidates])for i, score inenumerate(rerank_scores):            candidates[i]['rerank_score'] = score# 按重排序分数排序        candidates.sort(key=lambda x: x['rerank_score'], reverse=True)return candidates[:final_top_k]def_compute_rerank_scores(self, query: str, documents: list) -> list:        计算query和每个文档的相关性分数        scores = []with torch.no_grad():for doc in documents:# Reranker的输入格式是 [query, document]                inputs = self.reranker_tokenizer(                    [[query, doc]],                     padding=True,                     truncation=True,                     max_length=512,                     return_tensors='pt'                )                outputs = self.reranker_model(**inputs)                score = outputs.logits.squeeze().item()                scores.append(score)return scoresdefretrieve_with_query_expansion(self, collection_name: str, query: str,                                        llm_client, top_k: int = 5):        进阶技巧:查询扩展        用大模型改写用户问题,生成多个变体,再合并检索结果# 让大模型帮我们扩展查询        expansion_prompt = f请将下面这个问题改写成3个不同的表达方式,保持意思相同但用词不同。每行输出一个改写结果,不要序号,不要其他解释。原问题:{query}        expanded_queries = llm_client.generate(expansion_prompt).strip().split('\n')        expanded_queries = [q.strip() for q in expanded_queries if q.strip()]# 加上原始查询        all_queries = [query] + expanded_queries[:3]  # 最多取3个扩展查询print(f扩展后的查询:{all_queries})  # 调试用# 对每个查询分别检索        all_candidates = {}for q in all_queries:            results = self.vector_store.search(collection_name, q, top_k=10)for hit in results:                content = hit.entity.get('content')if content notin all_candidates:                    all_candidates[content] = {'content': content,'context_path': hit.entity.get('context_path'),'best_score': hit.score,'hit_count': 1                    }else:# 被多个查询命中的文档,增加权重                    all_candidates[content]['hit_count'] += 1                    all_candidates[content]['best_score'] = max(                        all_candidates[content]['best_score'],                         hit.score                    )# 综合评分:命中次数 * 最高得分        candidates = list(all_candidates.values())for c in candidates:            c['combined_score'] = c['hit_count'] * c['best_score']        candidates.sort(key=lambda x: x['combined_score'], reverse=True)return candidates[:top_k]

查询扩展这招特别好用。比如用户问数据库挂了怎么办,大模型可能会扩展成:

  • MySQL服务故障如何处理
  • 数据库无法连接的解决方案
  • 数据库宕机恢复步骤

这几个查询一起检索,能覆盖更多的相关文档。

四、第三个大坑:Prompt工程的门道比想象中深

检索的问题解决了,接下来就是把检索到的内容和用户问题一起喂给大模型了。这一步我本以为最简单,没想到也踩了不少坑。

最初的Prompt特别朴素:

defbuild_naive_prompt(query: str, context_docs: list) -> str:    最初的简单Prompt——后来证明太天真了    context = \n\n.join([doc['content'] for doc in context_docs])    prompt = f根据以下参考资料回答用户问题。参考资料:{context}用户问题:{query}请回答:return prompt

这个Prompt有几个严重问题:

问题一:大模型不知道什么时候该说不知道。 当参考资料里确实没有答案时,它还是会编一个出来。

问题二:没有引导大模型说明信息来源。 用户看到答案,不知道是从哪篇文档里来的,无法追溯和验证。

问题三:对于复杂问题,回答的结构不够清晰。

后来迭代了很多版,最终稳定下来的Prompt是这样的:

defbuild_rag_prompt(query: str, context_docs: list,                      include_sources: bool = True) -> str:    生产环境使用的Prompt模板    关键设计:明确角色定位、限制回答范围、要求标注来源# 格式化上下文,每段都标注来源    context_parts = []for i, doc inenumerate(context_docs, 1):        source = doc.get('context_path', '未知来源')        context_parts.append(f【资料{i},来源:{source}】\n{doc['content']})    context = \n\n\n\n.join(context_parts)    prompt = f你是一个企业内部知识库助手,专门帮助员工查找和理解公司内部文档。## 你的工作准则1. **只根据提供的参考资料回答问题**,不要使用你自己的知识。2. 如果参考资料中没有相关信息,请明确说根据现有资料,我无法找到关于这个问题的信息,并建议用户联系相关部门或换个关键词搜索。3. 回答时请标注信息来源,格式如【资料1】,方便用户追溯原文。4. 对于操作类问题,请按步骤清晰地列出;对于概念类问题,先给出简明定义再展开解释。5. 如果不同资料中的信息有冲突,请指出差异并说明各自的适用场景。## 参考资料{context}## 用户问题{query}## 回答要求请根据上述参考资料回答用户问题。记住:- 只使用参考资料中的信息- 标注信息来源- 没有把握的内容不要编造return promptdefbuild_conversational_prompt(query: str, context_docs: list,                                  chat_history: list = None) -> str:    支持多轮对话的Prompt    需要带上历史对话记录,让大模型理解上下文    context_parts = []for i, doc inenumerate(context_docs, 1):        source = doc.get('context_path', '未知来源')        context_parts.append(f【资料{i},来源:{source}】\n{doc['content']})    context = \n\n\n\n.join(context_parts)# 格式化历史对话    history_text = if chat_history:        history_parts = []for turn in chat_history[-5:]:  # 只保留最近5轮,避免太长            history_parts.append(f用户:{turn['user']})            history_parts.append(f助手:{turn['assistant']})        history_text = \n.join(history_parts)    prompt = f你是一个企业内部知识库助手。## 参考资料{context}## 对话历史{history_text if history_text else (这是对话的开始)}## 当前问题用户:{query}## 回答准则1. 优先根据参考资料回答,如无相关信息请明确说明2. 考虑对话历史的上下文(如用户说它可能指代之前提到的概念)3. 标注信息来源助手:return prompt

关于Prompt,我还想分享一个很重要的经验:不要试图在一个Prompt里塞太多指令。

一开始我把各种要求都写进去:回答要准确、要简洁、要友好、要专业、要标注来源、要分步骤、遇到不确定要说不知道……结果发现模型反而被绕晕了,有时候顾了这个忘了那个。

后来我的做法是:区分核心指令和优化指令,核心指令必须保留,优化指令可以根据问题类型动态调整。

classPromptBuilder:    Prompt构建器:根据问题类型动态调整# 核心指令——任何情况都必须包含    CORE_INSTRUCTIONS = 1. 只使用参考资料中的信息回答,不要编造2. 资料中没有的信息,明确说无法找到相关信息3. 标注信息来源【资料X】# 操作类问题的额外指令    PROCEDURE_INSTRUCTIONS = 回答格式要求:- 按步骤编号列出(第一步、第二步...)- 每个步骤要明确操作对象和操作动作- 重要的警告或注意事项用⚠️标出# 概念解释类问题的额外指令    CONCEPT_INSTRUCTIONS = 回答格式要求:- 先用一句话给出核心定义- 再详细解释关键点- 如有必要,举例说明# 故障排查类问题的额外指令    TROUBLESHOOT_INSTRUCTIONS = 回答格式要求:- 先列出可能的原因- 针对每个原因给出排查方法- 给出解决方案或规避建议    @classmethoddefbuild(cls, query: str, context_docs: list, question_type: str = general) -> str:        根据问题类型构建Prompt# 简单的问题分类逻辑(实际项目中可以用分类模型)if question_type == auto:            question_type = cls._classify_question(query)        extra_instructions = if question_type == procedure:            extra_instructions = cls.PROCEDURE_INSTRUCTIONSelif question_type == concept:            extra_instructions = cls.CONCEPT_INSTRUCTIONSelif question_type == troubleshoot:            extra_instructions = cls.TROUBLESHOOT_INSTRUCTIONS        context = cls._format_context(context_docs)        prompt = f你是企业内部知识库助手。## 必须遵守的规则{cls.CORE_INSTRUCTIONS}{f## 回答格式{extra_instructions} if extra_instructions else }## 参考资料{context}## 用户问题{query}请回答:return prompt    @classmethoddef_classify_question(cls, query: str) -> str:        简单的问题分类(基于关键词)        procedure_keywords = [怎么做, 如何操作, 步骤, 流程, 怎样]        concept_keywords = [是什么, 什么是, 定义, 解释, 区别]        troubleshoot_keywords = [为什么, 报错, 失败, 异常, 问题, 故障]        query_lower = query.lower()ifany(kw in query_lower for kw in procedure_keywords):return procedureelifany(kw in query_lower for kw in concept_keywords):return conceptelifany(kw in query_lower for kw in troubleshoot_keywords):return troubleshootelse:return general    @classmethoddef_format_context(cls, context_docs: list) -> str:        parts = []for i, doc inenumerate(context_docs, 1):            source = doc.get('context_path', '未知来源')            parts.append(f【资料{i},来源:{source}】\n{doc['content']})return \n\n\n\n.join(parts)

五、串起来:完整的RAG Pipeline

前面说了一堆细节,现在把它们串成一个完整的Pipeline:

from openai import OpenAIfrom typing importList, Dict, Optionalimport jsonclassRAGPipeline:    完整的RAG处理流程    文档切分 -> 向量化存储 -> 检索 -> 重排序 -> 生成回答def__init__(self,                  llm_base_url: str = https://api.deepseek.com,                 llm_api_key: str = your-api-key,                 llm_model: str = deepseek-chat):# 初始化各个组件        self.splitter = SmartDocumentSplitter(max_chunk_size=800)        self.vector_store = VectorStore()        self.retriever = EnhancedRetriever(self.vector_store)# 初始化LLM客户端(这里用DeepSeek,也可以换成其他的)        self.llm_client = OpenAI(base_url=llm_base_url, api_key=llm_api_key)        self.llm_model = llm_model        self.collection_name = knowledge_basedefingest_documents(self, documents: List[Dict]):        文档入库        documents格式:[{title: 文档标题, content: 文档内容, source: 来源}]print(f开始处理 {len(documents)} 篇文档...)        all_chunks = []for doc in documents:# 在内容前加上标题,帮助切分器识别结构            full_content = f# {doc['title']}\n\n{doc['content']}            chunks = self.splitter.split_markdown(full_content)# 给每个chunk加上文档来源信息for chunk in chunks:                chunk['source_doc'] = doc.get('source', doc['title'])            all_chunks.extend(chunks)print(f切分后共 {len(all_chunks)} 个片段)# 创建集合并插入        self.vector_store.create_collection(self.collection_name)        self.vector_store.insert_documents(self.collection_name, all_chunks)print(文档入库完成!)defquery(self,               question: str,               chat_history: Optional[List[Dict]] = None,              top_k: int = 5,              use_rerank: bool = True) -> Dict:        处理用户查询        返回:{answer: 回答内容, sources: [引用的来源], retrieved_docs: [检索到的文档]}# 1. 检索相关文档if use_rerank:            retrieved_docs = self.retriever.retrieve_with_rerank(                self.collection_name, question,                 initial_top_k=20, final_top_k=top_k            )else:            results = self.vector_store.search(self.collection_name, question, top_k=top_k)            retrieved_docs = [{'content': hit.entity.get('content'),'context_path': hit.entity.get('context_path'),'score': hit.score            } for hit in results]ifnot retrieved_docs:return {                answer: 抱歉,我没有找到与您问题相关的资料。您可以尝试换个关键词,或联系相关部门获取帮助。,                sources: [],                retrieved_docs: []            }# 2. 构建Promptif chat_history:            prompt = build_conversational_prompt(question, retrieved_docs, chat_history)else:            prompt = PromptBuilder.build(question, retrieved_docs, question_type=auto)# 3. 调用LLM生成回答        response = self.llm_client.chat.completions.create(            model=self.llm_model,            messages=[{role: user, content: prompt}],            temperature=0.3,  # 知识库问答用较低的temperature            max_tokens=2000        )        answer = response.choices[0].message.content# 4. 提取引用的来源        sources = list(set([doc.get('context_path', '未知来源') for doc in retrieved_docs]))return {            answer: answer,            sources: sources,            retrieved_docs: retrieved_docs        }defevaluate_response(self, question: str, answer: str,                           ground_truth: str = None) -> Dict:        回答质量评估(可选)        用LLM评估回答的质量,方便持续优化        eval_prompt = f请评估以下问答的质量。问题:{question}回答:{answer}{f参考答案:{ground_truth} if ground_truth else }请从以下维度评分(1-5分)并说明理由:1. 相关性:回答是否切题2. 准确性:信息是否正确3. 完整性:是否完整解答了问题4. 可读性:表述是否清晰易懂请用JSON格式输出:{{relevance: 分数, accuracy: 分数, completeness: 分数, readability: 分数, comments: 评价说明}}        response = self.llm_client.chat.completions.create(            model=self.llm_model,            messages=[{role: user, content: eval_prompt}],            temperature=0        )try:            eval_result = json.loads(response.choices[0].message.content)return eval_resultexcept:return {error: 评估结果解析失败}# 使用示例if __name__ == __main__:# 初始化Pipeline    rag = RAGPipeline(        llm_base_url=https://api.deepseek.com,        llm_api_key=your-api-key,        llm_model=deepseek-chat    )# 准备测试文档    test_documents = [        {            title: MySQL主从切换操作手册,            content: """				## 1. 前置检查				在执行主从切换之前,必须完成以下检查:				- 确认从库同步状态正常(Seconds_Behind_Master = 0)				- 确认没有正在执行的大事务				- 通知相关业务方,确认切换时间窗口				## 2. 切换步骤				### 2.1 在主库执行只读设置				SET GLOBAL read_only = 1;				### 2.2 等待从库完全同步				在从库执行 SHOW SLAVE STATUS,确认 Seconds_Behind_Master = 0				### 2.3 停止从库复制并提升为主库				STOP SLAVE;				RESET SLAVE ALL;				SET GLOBAL read_only = 0;				## 3. 切换后验证				- 确认新主库可以正常写入				- 确认应用连接已切换到新主库				- 监控新主库的性能指标				""",            source: DBA团队文档        }    ]# 入库    rag.ingest_documents(test_documents)# 测试查询    result = rag.query(MySQL切换前需要做哪些检查?)print(= * 50)print(问题:MySQL切换前需要做哪些检查?)print(= * 50)print(f\n回答:\n{result['answer']})print(f\n参考来源:{result['sources']})

六、上线后的一些经验教训

系统上线到现在差不多两个月了,期间又踩了不少坑,这里挑几个印象最深的说说。

教训一:用户的问题千奇百怪

我们在设计时假设用户会问MySQL怎么做主从切换这种正常问题。但实际上呢?

有人问:上次那个事故怎么处理的来着?——没有任何上下文,系统根本不知道那个事故是哪个。

有人问:帮我写个SQL。——这根本不是知识库问答,这是让大模型帮写代码。

还有人问:在吗?——我也不知道他想干啥。

后来我加了一个意图识别层,先判断用户的问题是否属于知识库问答的范畴:

defclassify_intent(self, query: str) -> str:"""识别用户意图"""    intent_prompt = f"""判断用户输入的意图类别,只输出类别名称:- knowledge_query:查询知识库信息(如询问流程、规范、操作方法)- code_request:请求生成代码- chitchat:闲聊或无明确意图- other:其他用户输入:{query}意图类别:"""    response = self.llm_client.chat.completions.create(        model=self.llm_model,        messages=[{"role": "user", "content": intent_prompt}],        temperature=0,        max_tokens=20    )return response.choices[0].message.content.strip()

对于非知识库问答的意图,给用户一个友好的提示而不是硬着头皮检索。

教训二:冷启动时的尴尬

系统刚上线时,知识库里的文档不多,覆盖的场景有限。用户问了几个问题都答不上来,体验特别差,于是就不来用了。

后来的解决办法:

  1. 上线前先梳理高频问题,确保至少这些问题能回答好
  2. 搞了一个问题收集功能,对于答不上来的问题,记录下来反馈给内容团队,让他们补充相关文档
  3. 做了一个兜底策略——如果检索不到高相关度的内容,就展示相关推荐,把一些相似度尚可的文档标题列出来,引导用户自己去看

教训三:文档更新的同步问题

知识库的文档是会更新的。老版本的操作手册废弃了,新版本发布了。但如果向量数据库里还存着老版本的内容,用户检索到的可能是过时信息。

这个问题说起来简单,做起来挺麻烦的。我们最后的方案是:

  1. 每个文档入库时记录版本号和更新时间
  2. 定期全量重新入库(我们是每周一次)
  3. 对于紧急更新的重要文档,支持手动触发单篇重入库

七、性能优化:让系统不那么慢

RAG系统有个让人头疼的问题——慢。

整个流程跑一遍:Embedding编码、向量检索、重排序、LLM生成,全部加起来可能要好几秒。用户体验就很差,问一个问题要等半天。

几个优化措施:

import asynciofrom functools import lru_cacheimport hashlibclassOptimizedRAG:    性能优化版RAGdef__init__(self):# 缓存热门查询的结果        self.query_cache = {}        self.cache_ttl = 3600# 1小时过期    @lru_cache(maxsize=1000)def_compute_query_embedding(self, query: str):        Embedding结果缓存        同样的问题不用重复计算向量return self.model.encode([query], normalize_embeddings=True)[0]def_get_cache_key(self, query: str) -> str:        生成缓存keyreturn hashlib.md5(query.lower().strip().encode()).hexdigest()asyncdefstream_query(self, question: str):        流式输出        不用等整个回答生成完,边生成边输出        retrieved_docs = await asyncio.to_thread(            self.retriever.retrieve_with_rerank,            self.collection_name, question, 20, 5        )        prompt = PromptBuilder.build(question, retrieved_docs, question_type=auto)# 使用流式API        stream = self.llm_client.chat.completions.create(            model=self.llm_model,            messages=[{role: user, content: prompt}],            temperature=0.3,            stream=True# 开启流式        )for chunk in stream:if chunk.choices[0].delta.content:yield chunk.choices[0].delta.content

流式输出这一点特别重要。用户问完问题后,马上就能看到回答在打字,心理上就不会觉得那么慢了。

八、回顾与思考

把这套系统从被骂下线到成为部门标配,前后折腾了将近一个月。趟过的坑挺多,但收获也很大。

几点核心总结:

1. RAG不是万能的,选好适用场景

RAG适合有明确知识库、答案可追溯的场景。如果你的需求是让大模型发挥创造力(比如写文案、做创意),那RAG反而是个约束。

2. 切分和检索是根基

大家往往把注意力放在大模型本身,觉得用更强的模型就能解决问题。但实际上,如果前面的切分和检索做得不好,再强的模型也是巧妇难为无米之炊。

3. Prompt工程真的是门手艺

同样的检索结果,不同的Prompt可能带来天壤之别的回答效果。这个没什么捷径,就是多试、多看、多迭代。

4. 上线只是开始

真正的挑战在上线之后。用户的各种奇葩输入、文档的持续更新、性能的优化、效果的监控……每一项都是持续的工作。

最后,附上这套系统目前的一些核心指标:

  • 日均查询量:200+次
  • 平均响应时间:2.3秒(开启流式后首字符延迟约0.8秒)
  • 用户满意度(通过回答后的点赞/点踩收集):约72%
  • 无法回答的比例:约22%(这部分会定期分析,推动补充文档)

学AI大模型的正确顺序,千万不要搞错了

🤔2026年AI风口已来!各行各业的AI渗透肉眼可见,超多公司要么转型做AI相关产品,要么高薪挖AI技术人才,机遇直接摆在眼前!

有往AI方向发展,或者本身有后端编程基础的朋友,直接冲AI大模型应用开发转岗超合适!

就算暂时不打算转岗,了解大模型、RAG、Prompt、Agent这些热门概念,能上手做简单项目,也绝对是求职加分王🔋

在这里插入图片描述

📝给大家整理了超全最新的AI大模型应用开发学习清单和资料,手把手帮你快速入门!👇👇

学习路线:

✅大模型基础认知—大模型核心原理、发展历程、主流模型(GPT、文心一言等)特点解析
✅核心技术模块—RAG检索增强生成、Prompt工程实战、Agent智能体开发逻辑
✅开发基础能力—Python进阶、API接口调用、大模型开发框架(LangChain等)实操
✅应用场景开发—智能问答系统、企业知识库、AIGC内容生成工具、行业定制化大模型应用
✅项目落地流程—需求拆解、技术选型、模型调优、测试上线、运维迭代
✅面试求职冲刺—岗位JD解析、简历AI项目包装、高频面试题汇总、模拟面经

以上6大模块,看似清晰好上手,实则每个部分都有扎实的核心内容需要吃透!

我把大模型的学习全流程已经整理📚好了!抓住AI时代风口,轻松解锁职业新可能,希望大家都能把握机遇,实现薪资/职业跃迁~

这份完整版的大模型 AI 学习资料已经上传优快云,朋友们如果需要可以微信扫描下方优快云官方认证二维码免费领取【保证100%免费】

在这里插入图片描述

告别高延迟!用RV1106+4G模块+libdatachannel搭建低功耗WebRTC监控,保姆级避坑指南 本文详细介绍了如何利用RV1106低功耗处理器、4G模块和libdatachannel库构建嵌入式WebRTC监控系统,解决高延迟问题。通过硬件选型、系统优化和核心代码实现,提供了一套完整的低功耗实时视频传输方案,特别适合智能安防和工业物联网应用。 阅读详情

相关推荐

kettle从入门到精通 第九十二课 ETL之kettle 使用Kettle的Carte对外发布读写接口

场景:使用kettle实现将查询结果返回给客户端,也就是说kettle暴露查询接口供外围系统调用。前提必须是使用carte服务才可以提供接口供外部系统调用。具体实操方法如下:1、设计转换根据具体需求设计转换,主要用到的步骤有获取变量(从请求参数中获取变量)、json输入(解析入参),json 输出将结果转换为json格式输出到客户端。如下图所示:2、接收参数使用获取变量步骤来接收接口请求参数,变量通过${}方法进行获取,如下图${PARAM}则是获取PARAM参数,将参数赋值为req字段。

zhangjin1222的博客 954

大模型入门到进阶:什么是 RAG?为什么需要 RAG?RAG 的流程

学习AI大模型是一个系统的过程,需要从基础开始,逐步深入到更高级的技术。这里给大家精心整理了一份全面的AI大模型学习资源,包括:AI大模型全套学习路线图(从入门到实战)、精品AI大模型学习书籍手册、视频教程、实战学习等录播视频,免费分享!

star_nwe的博客 3万+

量化金融笔记1-股票量化基础

股票量化基础 文章目录什么是股票股票的定义及其诞生所解决的问题股票的主体和流通股票的常规分类股票价格收益率什么是股票价格的收益率股票价格收益率计算在python中的实现两种收益率算法的特点及应用场合股票交易规则场内交易与场外交易A股、B股、H股国内多层次的资本市场股票交易指令和撮合机制股票交易时间股票交易成本股票复权股票数据介绍与提取行业分类和指数行情数据因子数据标的数据基本面数据 什么是股票 股票的定义及其诞生所解决的问题 股票的定义:股票是一种有价证券,是股份有限公司公开发行的用以证明投资者的股东身份

折桂狂人的博客 2395

大模型应用RAG系列(1)初识RAG

之前我们在讲大模型的应用方向和架构时,有提到RAG、Agent、Fine-Tune。在作者写大模型专题的文章时,也是边学习,边梳理,边总结。在这个过程中,大模型在各个方向都不断地快速发展,对应的paper、理论、方向也是不断地涌现出来。

AAI666666的博客 1803

双模音频解码器设计:USB+蓝牙LDAC平衡输出方案

音频解码器是数字音频系统中实现数模转换的核心组件,其性能直接决定音质保真度与系统延迟。基于高集成SoC的解码方案通过统一时钟域与片内DSP处理,有效抑制Jitter并简化硬件拓扑;结合全平衡模拟输出架构,可显著提升共模抑制比(CMRR)与信噪比(SNR)。该技术路径广泛应用于便携式Hi-Res音频设备、USB-C小尾巴DAC及蓝牙高解析耳机放大器等场景。本文围绕ES9038Q2M双模SoC展开,详解其在LDAC协议支持、UAC2.0兼容性及平衡电流型DAC输出方面的工程实现,为高性能、可量产音频硬件开发提供

weixin_42613018的博客 388

【原创】关于西门子S120 的电气图纸设计

我们首先要了解一下模块的基本信息:SINAMICS 功率模块 PM240-2 三相交流380-480V+10/-10% 47-63Hz;至此,我们选择的驱动系统配置的资料都已经查找完毕,接下来就是根据每个部件的功能接口和我们需要实现的功能画出接线连接图,这样关于伺服电机控制,硬件部分设计就完成了。2)FSA - FSC 型功率模块 PM240-2 的屏蔽连接板外形尺寸图,所有数据以毫米(英寸)为单位。1)FSA 型功率模块 PM240-2 的外形尺寸图,所有数据以毫米(英寸)为单位。

Qinmingyue13的博客 2640

大模型rag技术,什么是RAG?大模型入门到精通,收藏这篇就够了

RAG(Retrieval-Augmented Generation)即检索增强生成,为大模型提供了从特定数据源检索到的信息,以此来修正和补充生成的答案。

Python_0011的博客 7564

电机控制的智慧之眼:全面解析FOC状态观测器

摘要:本文深入探讨磁场定向控制(FOC)中的状态观测器技术,重点分析其在无传感器电机控制中的关键作用。文章详细介绍了滑模观测器(SMO)、扩展卡尔曼滤波(EKF)、龙贝格观测器和高频注入法等主流观测器的工作原理、技术特点及适用场景,其中SMO因结构简单、鲁棒性强成为当前主流方案。同时阐述了观测器与锁相环(PLL)结合的工程实践方案,并针对不同应用场景提供了观测器选型建议。文章还探讨了启动问题处理、参数敏感性等工程挑战的解决方案,以及混合观测器、AI应用等未来发展趋势。这些观测器技术使电机系统在无机械传感器情

汽车软件开发 275

大模型系列——解读RAG

RAG 是2023年最流行的基于 LLM 的应用系统架构。有许多产品几乎完全建立在 RAG 之上,覆盖了结合网络搜索引擎和 LLM 的问答服务,到成千上万个数据聊天的应用程序。很多人将RAG和Agent 作为大模型应用的两种主流架构,但什么是RAG呢?RAG又涉及了哪些具体的技术呢?1. 什么是RAGRAG即检索增强生成,为 LLM 提供了从某些数据源检索到的信息,并基于此修正生成的答案。RAG ...

我相信...... 3751

Qt Creator牵手DeepSeek,开启AI开发新征程

Qt Creator 是一款功能强大的跨平台集成开发环境(IDE),专为 Qt 应用程序开发量身定制。它支持在编程过程中使用 GitHub Copilot 等编码助手,能帮助开发者为桌面、移动端和嵌入式平台创建软件。在代码编辑方面,Qt Creator 具备代码补全、语法突出显示、重构等功能,内置文档也随手可得,极大提高了代码编写的效率和准确性。

远程部署调试 运行安装 项目调试 二次开发 项目技术新 持续迭代 部分源码免费分享 7189

系统安全性评估 (SSA) 详细解析

系统安全性评估(SSA)是一种自下而上的验证过程,目的是确保系统设计中的每个功能、子系统、硬件和软件的实现,符合早期通过 FHA 和 PSSA 设定的安全性需求。它通过对已实现的系统进行系统性综合评价,检验各项设计和安装是否符合预期的安全性目标,最终支持飞机的适航认证。飞行控制系统是飞机安全性设计的核心子系统之一,负责控制飞机的姿态和方向。为了确保飞行控制系统能够在各种飞行条件下保持安全运行,SSA 被用于验证该系统的设计是否满足 FHA 中设定的安全性目标。

qq_41854911的博客 1651

【从零开始】AI大模型RAG应用实战经验分享:全面解析与实操指南!非常详细收藏我这一篇就够了!

我从发布后,就开始申请使用ChatGpt的功能,使用的功能进行日常编程的尝试,也积极帮朋友和客户找梯子与”优秀的机器人“聊天。算是较早拥抱AI 大语言模型的一波人。随着探索的不算深入,2024年的团队目标是在项目中把AI 大模型用起来。在一个客户2023年度总结小会的时候,各种的推荐和机缘巧合,说服了一个客户为他们的项目嵌入AI 买单的意愿。项目的需求:一个细分法律知识RAG。我们的目标:1. 用最好的AI 大模型基座;

2401_85390073的博客 1521

最新Python爬虫实战(入门爬虫篇)——案例8:图书网畅销榜数据采集(详细爬虫思路截图+抓包动图演示+完整爬虫代码+详细注释)

本次案例通过 Python 爬虫技术,成功实现了对图书网畅销榜多页数据的批量爬取与 Excel 保存。代码采用模块化设计,包含分页参数构造、API 接口请求、JSON 数据解析、Excel 数据持久化等完整流程;通过配置模拟浏览器的请求头,有效规避基础反爬策略,保证请求的稳定性。整个爬虫逻辑遵循 “分页遍历 → 接口请求 → JSON 解析 → 提取图书信息 → 保存到 Excel” 的标准流程,与电商类接口爬取范式高度一致。

努力让自己发光,对的人才能迎着光而来 29万+

还在为RAG效果差烦恼?中小企业AI部署“踩坑”后,我总结了10条让RAG“能用好”的经验

摘要:企业级人工智能面临"上下文割裂难题",即AI难以理解特定商业场景。情境AI CEO杜威·基拉分享10条RAG系统部署经验:1)着眼整个系统而非单一模型;2)专注专业领域而非通用AI;3)将数据视为核心竞争力;4)按生产标准而非试点水平设计;5)快速迭代优先于追求完美;6)聚焦商业价值而非技术细节;7)确保易用性和集成性;8)创造"惊艳"用户体验;9)重视系统可观测性;10)设定高远目标。这些经验可帮助企业将AI挑战转化为商业价值。

m0_65555479的博客 858

Camera2Demo

在Camera2 的基础上进行封装,使得Camera2一堆初始化逻辑与Activity分离,降低耦合度,完成预览拍照并保存的操作

精雕软件(JDpaint) 5.5 绿色版

精雕软件(JDpaint) 5.5 绿色版精雕软件(JDpaint) 5.5 绿色版精雕软件(JDpaint) 5.5 绿色版

六位一线AI工程师总结大模型应用摸爬滚打一年的心得,网友:全程高能!

六位一线AI工程师和创业者,把在大模型应用开发上摸爬滚打一整年的心得,全!分!享!了!(奇怪的六一儿童节大礼包出现了)这篇干货长文,一时间成为开发者社区热议的话题。有网友评价为,大模型领域少有的**“有操作性”**的实用见解,非常值得一读。这6位作者来自不同背景,比如有大厂工程师,也有独立开发者,还有咨询顾问。但他们的共同之处,是过去一年里一直在大模型之上构建真实应用程序,而不只是炫酷的Demo演示,他们认为:在他们的一系列分享中,网友热议的亮点包括但不限于:-何时用长上下文、何时RAG、何时微调模型总之,

python122_的博客 1268

Devexpress16.2.6破解工具

Devexpress16.2.6破解工具Devexpress16.2.6破解工具Devexpress16.2.6破解工具Devexpress16.2.6破解工具

从Java到AI大模型:程序员5年自学转型之旅,成功上岸经验分享!

在这个科技飞速发展的时代,技术的更新换代犹如潮水般汹涌。作为一名有着5年Java开发经验的程序员,我意识到自己需要适应这种变化,寻找新的职业增长点。随着人工智能(AI)和机器学习领域的迅猛发展,以及AI大模型在各个行业应用的不断扩展,我看到了这个领域蕴藏的巨大潜力和机会。因此,在2024年的春天,我决定转行进入AI大模型领域,开启一段全新的旅程。

2401_85390073的博客 1284

数学建模模型总结

这是本人在两年内准备参加数学建模竞赛搜集到的模型资料,已经经过了本人的筛选和整理,希望能够对准备参加数学建模竞赛的学弟学妹们提供一些帮助!

收藏!AI Agents开发10条实战经验+大模型全套学习资料,助你快速入门

文章分享了开发AI Agents的10条实用经验,包括Tools先行、从通用底层工具开始、使用单一Agent测试、选择合适模型等。同时提供了大模型全套学习资料,涵盖基础理论、RAG开发、Agent设计、模型微调等5大模块,包含视频教程、PDF书单、行业报告和面试题等资源,适合从零基础到进阶的学习者,帮助开发者掌握AI大模型技术,抓住行业机遇。

2401_84815887的博客 562

LLM实战——使用LangChain4j构建本地RAG系统

RAG(Retrieval-Augmented Generation)的核心思想是:将传统的信息检索(IR)技术与现代的生成式大模型(如chatGPT)结合起来。具体来说,RAG模型在生成答案之前,会首先从一个大型的文档库或知识库中检索到若干条相关的文档片段。再将这些检索到的片段作为额外的上下文信息,输入到生成模型中,从而生成更为准确和信息丰富的文本。RAG的工作原理可以分为以下几个步骤:1.接收请求:首先,系统接收到用户的请求(例如提出一个问题)。

2401_85280106的博客 1267

Java开发者LLM实战——使用LangChain4j构建本地RAG系统

RAG(Retrieval-Augmented Generation)的核心思想是:将传统的信息检索(IR)技术与现代的生成式大模型(如chatGPT)结合起来。具体来说,RAG模型在生成答案之前,会首先从一个大型的文档库或知识库中检索到若干条。

Java癫疯的博客 1434

企业级RAG智能体落地实战:10个血泪教训让你避开99%的坑

今天我就基于这个分享,结合自己的实战经验,给大家深度解读企业级RAG智能体落地的核心要点。无论你是AI产品经理、技术负责人,还是想要在企业中推进AI应用的开发者,这篇文章都会给你很多启发。

2301_76168381的博客 985

什么才是AI大模型入门神仙书?大模型从入门到精通(非常详细)看这一篇就够了

(7)Agent:然后就到了真正让大模型智能的AI Agent部分:AutoGPT、MetaGPT、LangGraph、AgentScope,都是从最开始的环境搭建、Hello World开始,一步一步深入实战,甚至深入源代码的给大家展示和讲解。书中的内容以**实战**为主,不仅对相关任务有详细的背景和系统设计介绍,还教你使用ChatGPT相关API创造新的功能和应用,并且提供**实例代码和实现流程**,让每一位学习者都能做出自己的AI产品!(4)RAG框架让大模型减少幻觉,是当下AI大模型应用的主流。

大模型教程的博客 517

从零基础到接下第一单:我的AI大模型学习之路(2026真实经历分享)

写这篇文章时么难写这篇文章时,我刚结束第6个项目的交付。普通人通过系统学习,真的能在AI时代找到自己的位置。如果你也想入门AI大模型,或者已经在学但不知道怎么变现,欢迎来交流。

易辰的博客 1064

【上岸实录】后端开发转行去做大模型算法了,打工第三年大模型学习全攻略,建议收藏!

这篇文章是一位中科院自动化所研究生(大厂面试官)的2024年个人总结,主要分享了大模型学习与工作实践。作者详细介绍了AI工具应用、面试经验、工作技巧,以及学业成果。文章强调AI Agent是未来趋势,分享了自媒体运营经验,并鼓励普通人接触国内大模型平台提高效率。作者通过个人经历展示了如何利用AI工具实现职业发展与效率提升。

libaiup的博客 776

本地玩AI,电脑到底怎么配?一个土木工程师的踩坑与提效实战分享,大模型入门到精通,收藏这篇就足够了!

今天这篇提效日记,我趁双十一以及抖音的消费劵花费6000多元配的一台AI“小钢炮”的完整心路历程分享给你,希望能帮你把钱花在刀刃上,一步到位,少走弯路。

Y525698136的博客 7187
上一篇: Meta大模型思考新范式全解(非常详细):SoT与T2S-Bench深度剖析,从入门到精通,收藏这一篇就够了!
下一篇: Harness Engineering工程化教程(非常详细),AI Agent复杂长任务从入门到精通,收藏这一篇就够了!
Python_金钱豹
博客等级 码龄5年 1万+粉丝 2835原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值