大家好,我是小编~
上一篇我简单介绍了AI的起源和一些基础认知,有朋友留言说:
"这些我都懂,我现在的问题是—AI为什么老是胡说?"
这个问题问得很实在,而且我敢肯定:
如果你在做AI项目,一定已经被它坑过
我自己在做AI知识库的时候,就遇到过这种情况:
明明文档里没有的内容,它能给你编一套完整方案
同一个问题,每次答案还不一样
有时候甚至"自信满满地错"
一开始我以为是模型不行,后来才发现:
问题根本不在模型,而在于你怎么用它
这篇我不讲概念,直接讲一个你必须搞懂的东西:
什么是RAG,我们得首先了解大模型的本质是什么:
大模型本质不是"查资料",而是"生成文本"
你问它问题,它不是去数据库查答案,而是:
根据训练过的数据,"猜一个最像答案的话"
注意这里我说的是"猜",可能不准确,但好理解。
根据训练数据,预测"在当前上下文中最有可能出现的下一个词"(Next Token Prediction)
然而这种预测就带来一个很现实的问题,不管是公司还是个人,很多资料是不能在互联网上公开的:
- 它不知道你公司的接口文档
- 不知道你的业务逻辑
- 更不知道你的私有数据
那它怎么给你答案?
它只能"合理地编"
这就是我们常听到的:
幻觉(Hallucination)
而且越是表达能力强的模型:
越会编,而且编得越像真的
例如你的代码这样写
import dev.langchain4j.model.chat.ChatLanguageModel;
import dev.langchain4j.model.openai.OpenAiChatModel; public class HallucinationDemo { public static void main(String[] args) { ChatLanguageModel model = OpenAiChatModel.builder() .apiKey("YOUR_API_KEY") .modelName("gpt-4o-mini") .build(); String question = "我们公司内部接口 /api/internal/pay/v2 的调用流程是什么?"; String answer = model.generate(question); System.out.println(answer); } }
AI回答的有板有眼,语气非常自信,结构非常完整,看起来"完全正确"。
接口 /api/internal/pay/v2 的调用流程如下:
- 用户鉴权(Token校验)
- 参数校验(金额、订单号等)
- 调用支付服务
- 返回支付结果
但是,很明显这不是我们要的答案,因为模型它本就不知道答案,但必须生成一个"像答案的东西"
它其实是在套模板:
"接口调用流程通常是这样"
然后拼一个"合理答案"
我们换个角度。
如果是你自己回答一个问题的话,你会怎么做?
比如有人问你:
"我们系统A的接口调用流程是什么?"
你的第一反应肯定不是"开始编",而是:
先去翻文档
而RAG做的事情,和你的反应一模一样:
让AI也"先查资料,再回答"
换句话说:
RAG = 给AI装一个"可搜索的知识库"
RAG的架构图类似这样
看起来很复杂,但其实你只要记住下面这个就够了:
- 第一步:把知识"存进去"
- 第二步:用户提问时,先去"找相关内容"
- 第三步:把"资料 + 问题"一起丢给AI
关于第一步,如何把知识存进去
你需要做三件事:
- 把文档切成一小段一小段(chunk)
- 把每一段转成向量(embedding)
- 存进数据库(向量库)
你可以把这个步骤理解为:
把"文字"变成"可计算的坐标"
那么我们为什么需要把文档切成一小段一小段的呢?
不切 chunk,检索就不准;检索不准,AI一定胡说
假设你有一份文档:
《系统设计文档》
- 用户登录流程
- 支付流程
- 订单系统
- 消息队列
- 接口A说明
- 接口B说明
你整篇直接丢进向量库。
然后用户问:
"接口A怎么调用?"
向量检索会发生什么?
它会拿"整篇文档"去做相似度计算
问题来了:
文档里包含一堆无关内容(登录、支付、订单…)
"接口A"只是其中一小部分
最终结果就是:
相似度被"稀释"了
结果:
要么查不到(分数不够)
要么查到一堆无关内容
为什么切 chunk 就好了?
我们把刚才那份文档拆开:
chunk1:用户登录流程
chunk2:支付流程 chunk3:接口A说明 chunk4:接口B说明
再问同样的问题:
"接口A怎么调用?"
这次会发生什么?
检索系统会:
把问题转成向量
和每个 chunk 分别算相似度
结果:
chunk3(接口A)会被精准命中
本质变化:
从:
❌ "一整本书参与匹配"
变成:
✅ "一小段一小段精确匹配"
再说一个比较关键的点
大模型是有上下文长度限制的
比如:
4k / 8k / 128k token
如果你不切 chunk:
你可能会把一整篇文档塞进去
结果:
超长 → 直接截断
或者 → 成本爆炸
chunk的作用之一就是:
控制输入长度 + 提高信息密度
chunk本质是让向量搜索具备"段落级命中能力
第二步:用户提问时,先去"找相关内容"
用户问:
"接口A怎么调用?"
系统不会直接问AI,而是先做一件更重要的事:
去向量数据库里找"最像这个问题的几段内容"
EmbeddingStore
store = PgVectorEmbeddingStore.builder() .datasource(getDataSource()) .table("knowledge") .dimension(1536) .build();
EmbeddingModel embeddingModel = getEmbeddingModel();
ContentRetriever retriever = EmbeddingStoreContentRetriever.builder()
.embeddingStore(store) .embeddingModel(embeddingModel) .maxResults(3) .minScore(0.7) .build(); String question = "接口A怎么调用?"; List
contents = retriever.retrieve(question); System.out.println("==== 检索结果 ===="); for (Content content : contents) { System.out.println(content.textSegment().text()); System.out.println("------------------"); }
第三步:把"资料 + 问题"一起丢给AI
String question = "接口A怎么调用?";
String context = contents.stream()
.map(Content::textSegment) .map(segment -> segment.text()) .reduce("", (a, b) -> a + "
" + b);
String prompt = String.format("""
你是企业内部AI助手,请严格根据"资料"回答问题。 如果资料中没有相关信息,请回答:"无法确定",不要编造。 ===== 资料 ===== %s ===== 问题 ===== %s ===== 输出要求 ===== - 只基于资料回答 - 不允许编造 - 不确定就说无法确定 """, context, question);
ChatLanguageModel model = OpenAiChatModel.builder()
.apiKey("YOUR_API_KEY") .modelName("gpt-4o-mini") .build(); String answer = model.generate(prompt); System.out.println("==== AI回答 ===="); System.out.println(answer); }
}
这一步非常关键。
最终给模型的,不是:
❌ "请回答这个问题"
而是:
✅ "基于以下资料回答,不要乱编"
你可以理解为:
👉你在"喂答案范围",而不是让它自由发挥
不讲虚的,直接上代码。
我这边用的是:
- Spring Boot
- LangChain4j
- PostgreSQL + pgvector
- Flyway
导入依赖
org.springframework.boot
spring-boot-starter-web
org.springframework.boot
spring-boot-starter-data-jpa
org.springframework.boot
spring-boot-starter-validation
org.postgresql
postgresql
runtime
dev.langchain4j
langchain4j
${langchain4j.version}
dev.langchain4j
langchain4j-open-ai
${langchain4j.version}
dev.langchain4j
langchain4j-pgvector
${langchain4j.version}
org.flywaydb
flyway-core
org.projectlombok
lombok
true
org.springframework.boot
spring-boot-starter-test
test
初始化聊天大模型及向量数据操作相关的配置
@Bean public ChatLanguageModel chatLanguageModel() { return OpenAiChatModel.builder() .baseUrl(openaiUrl) .apiKey(openaiApiKey) .modelName(modelName) .build(); } @Bean public EmbeddingStore
embeddingStore() { return PgVectorEmbeddingStore.builder() .host("localhost") .port(5432) .database("ai_rag_db") .user("postgres") .password("") .table("knowledge") .dimension(1536) .createTable(false)// 禁用自动创建,使用 Flyway 管理 .build(); } @Bean public EmbeddingModel embeddingModel() { return OpenAiEmbeddingModel.builder() .baseUrl(openaiUrl) .apiKey(openaiApiKey) .modelName("text-embedding-3-small") .build(); } @Bean public EmbeddingStoreContentRetriever contentRetriever( final EmbeddingStore embeddingStore, final EmbeddingModel embeddingModel) { return EmbeddingStoreContentRetriever.builder() .embeddingStore(embeddingStore) .embeddingModel(embeddingModel) .maxResults(3) .minScore(0.7) .build(); }
Flyway配置,启动项目自动运行数据库脚本
@Configuration
public class FlywayConfig {
@Bean(initMethod = "migrate") @DependsOn("dataSource") public Flyway flyway(DataSource dataSource) { return Flyway.configure() .dataSource(dataSource) .baselineOnMigrate(true) .baselineVersion("1") .locations("classpath:db/migration") .outOfOrder(false) .validateOnMigrate(true) .cleanDisabled(true) .load(); } @Bean public FlywayMigrationInitializer flywayInitializer(Flyway flyway) { return new FlywayMigrationInitializer(flyway); }
}
RagController,负责聊天和文档的增删改查
@Slf4j
@RestController @RequestMapping("/api/rag") @RequiredArgsConstructor public class RagController {
private final RagService ragService; @PostMapping("/ask") public ResponseEntity
askQuestion(@RequestBody RagRequest request) { try { String answer = ragService.askQuestion(request.question()); return ResponseEntity.ok(new RagResponse(answer, true)); } catch (Exception e) { log.error("Error processing question: {}", request.question(), e); return ResponseEntity.ok(new RagResponse("抱歉,处理您的问题时出现了错误。", false)); } } @PostMapping("/documents") public ResponseEntity
uploadDocument(@RequestParam("file") MultipartFile file) { try { ragService.ingestDocument(file); return ResponseEntity.ok("文档上传并处理成功"); } catch (Exception e) } @GetMapping("/documents") public ResponseEntity
> getAllDocuments() @DeleteMapping("/documents/{id}") public ResponseEntity
deleteDocument(@PathVariable Long id) { try { ragService.deleteDocument(id); return ResponseEntity.ok("文档删除成功"); } catch (Exception e) { return ResponseEntity.badRequest().body("文档删除失败"); } } // Request/Response DTOs public record RagRequest(String question) {} public record RagResponse(String answer, boolean success) {}
}
Service处理和AI及数据库相关的操作
@Slf4j
@Service @RequiredArgsConstructor public class RagService {
private final ChatLanguageModel chatLanguageModel; private final EmbeddingModel embeddingModel; private final EmbeddingStore
embeddingStore; private final EmbeddingStoreContentRetriever contentRetriever; private final DocumentRepository documentRepository; private static final PromptTemplate PROMPT_TEMPLATE = PromptTemplate.from(""" 你是企业内部AI助手,负责基于提供的资料回答问题。 【核心规则】 1. 只能使用"资料"中的信息回答 2. 严禁使用任何外部知识或常识补充 3. 如果资料中没有明确答案,不要编造 【无法确定时的要求】 当资料无法回答问题时,请从以下表达中任选一种,自然回答: - 无法确定 - 资料中未提及该信息 - 当前资料无法提供该问题的答案 - 未在提供的资料中找到相关内容 ⚠️ 注意: - 不要重复使用同一句话 - 不要解释原因(例如"因为资料不足"这种可以,但不要长篇解释) - 保持简洁 【回答要求】 1. 回答简洁、直接 2. 优先使用资料原文 3. 不输出无关内容 ===== 资料 ===== {{context}} ===== 问题 ===== {{question}} ===== 输出 ===== 直接输出答案 """); public String askQuestion(String question) { try { // 1. 检索相关内容 List
contents = contentRetriever.retrieve(Query.from(question)); // 2. 构建上下文 String context = contents.stream() .map(Content::textSegment) .map(TextSegment::text) .collect(Collectors.joining("
"));
// 3. 构造提示词 final String prompt = PROMPT_TEMPLATE.apply( java.util.Map.of( "context", context, "question", question ) ).text(); // 4. 调用大模型 final AiMessage response = AiMessage.from(chatLanguageModel.generate(prompt)); return response.text(); } catch (Exception e) { log.error("Error processing question: {}", question, e); return "抱歉,处理您的问题时出现了错误,请稍后重试。"; } } public void ingestDocument(MultipartFile file) log.info("Document ingested successfully: {}", file.getOriginalFilename()); } catch (Exception e) { log.error("Error ingesting document: {}", file.getOriginalFilename(), e); throw new RuntimeException("Failed to ingest document", e); } } public List
getAllDocuments() { return documentRepository.findAll(); } public void deleteDocument(Long id) { documentRepository.deleteById(id); }
}
Springboot的配置文件
server:
port: 8080
spring: application:
name: ai-rag
datasource:
url: jdbc:postgresql://localhost:5432/ai_rag_db username: postgres password: driver-class-name: org.postgresql.Driver
jpa:
hibernate: ddl-auto: validate show-sql: true properties: hibernate: dialect: org.hibernate.dialect.PostgreSQLDialect format_sql: true
flyway:
enabled: true baseline-on-migrate: true baseline-version: 1 locations: classpath:db/migration out-of-order: false validate-on-migrate: true clean-disabled: true # 与 LangChain4j 配置保持一致 url: ${spring.datasource.url} user: ${spring.datasource.username} password: ${spring.datasource.password}
OpenAI Configuration
openai: api-key: ${OPENAI_API_KEY:sk-xxx} model-name: gpt-3.5-turbo url: https://api.url
当我不上传任何文档的时候
我上传一个文档,里面描述了马明聪是谁
这部分你一定会遇到。
❗chunk切分不合理
一开始我直接按整段文档丢进去,结果就是:
👉 查出来的内容完全不相关
后来改成:
200~500字一段,效果明显提升
❗相似度阈值乱设
.minScore(0.7)
这个值没有标准答案,但你要知道:
- 太高 → 查不到内容
- 太低 → 垃圾内容混进来
👉 最好的办法:自己打印日志调试
❗以为用了RAG就不会胡说
这是一个大坑。
现实是:
👉RAG只能减少胡说,不会消灭
如果你:
检索不准
prompt没约束
数据本身有问题
那AI照样乱来。
❗忽略metadata过滤
比如你有:
多个系统
多个版本
但你不加过滤条件:
AI会把A系统的答案用在B系统上
很多人以为:
做AI应用 = 调一个大模型API
但真实情况是:
模型只占20%,剩下80%是工程问题
包括:
- 数据怎么处理
- 检索怎么做
- prompt怎么设计
你只记住一句话:
RAG不是技术名词,而是一种"让AI不胡说的工程手段"
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容,请联系我们,一经查实,本站将立刻删除。
如需转载请保留出处:https://51itzy.com/kjqy/270622.html