用 SQLite+Embedding 给 Agent 加上 RAG,从此秒懂项目源码
这一期我们来给 Agent 装上 RAG让 Agent 可以直接读我们的代码库。举个具体场景我问“MemoryManager 是怎么压缩上下文的”。没有 RAG 的 Agent 只能凭训练数据瞎猜猜得对算运气好。装了 RAG 之后Agent 会先去代码库里捞 ContextCompressor.compressIfNeeded看 Map-Reduce 的实现再基于这段真实代码的回答。整个 RAG 的架构示意图如下所示。01、RAG 的整体设计RAG 大家应该不陌生了一句话讲清楚。把知识库向量化然后持久化到向量数据库查询的时候按照语义相似度找出最相关的片段再连同问题一起塞给 LLM。落到代码场景有三个问题绕不开。第一个是怎么切。代码不像文档按字数硬切会切出了很多噪音。最稳妥的办法是按结构特征切——文件级、类级、方法级检索时按粒度匹配。第二个是存到哪。生产环境通常上 Milvus、Pinecone 、ElasticSearch 这种专用向量库。但我们是个 CLI 工具这些都太重量级了。所以我这里选择了 SQLite。第三个是怎么样才能搜得准。纯向量检索对自然语言友好对代码标识符却不一定。所以我们这里做了混合检索——语义打底、关键词加权、再按 chunk 类型加分。method 块比 file 块优先级高因为用户问“怎么实现的”给方法体比给整个文件有用得多。举个例子搜“处理用户登录的地方”它能定位到LoginService.authenticate。整个 RAG 模块拆成 10 个类下面一块一块讲。CodeChunk —— 代码块数据模型CodeChunker —— AST 分块EmbeddingClient —— 向量化客户端VectorStore —— SQLite 向量存储CodeAnalyzer —— AST 关系分析CodeRelation —— 关系数据模型CodeIndex —— 索引入口CodeRetriever —— 检索入口RagQueryTokenizer —— 查询分词SearchResultFormatter —— 结果格式化02、AST 解析代码分块是 RAG 里最容易被低估的一步。分得好检索准分得糙后面再多加权也救不回来。Java 文件和非 Java 文件得分开处理。Java 走 AST按类和方法切非 Java比如 Markdown、yaml就按字符大小切每段控制在 2000 字符以内。Java 这块用 JavaParserCodeChunker 里的核心逻辑是这样public ListCodeChunk chunkFile(Path filePath) throws IOException { String content Files.readString(filePath); // 非 Java 文件按大小分段 if (!relativePath.endsWith(.java)) { return chunkLargeText(relativePath, content); } // Java 文件AST 解析分块 return chunkJavaFile(filePath, content);}JavaParser 可以把语言级别设到 JAVA_17text block、record、sealed class 这些新语法都能正常解析。万一遇到语法错误可以自动回退到按大小分段不会因为一个文件解析失败就漏掉整块代码。非 Java 文件超过 2000 字符就生成一个 chunk同时把起止行号一起带上。检索结果直接能跳到对应行不用二次定位。Java 这边类级和方法级各存一份。类级只保留类声明和前 5 行字段、签名这些信息够用了不用把几百行的类全塞进去方法级则把完整方法体捞出来单独成块。// 类级别 chunkchunks.add(CodeChunk.classChunk( filePath.toString(), className, classHeader, classStart, classEnd));// 方法级别 chunkchunks.add(CodeChunk.methodChunk( filePath.toString(), className . methodSignature, methodContent, methodStart, methodEnd));CodeChunk 用的是 record除了正文内容还带了文件路径、块类型、名称、起止行号。toEmbeddingText 方法会把这些拼成[method:Agent.run] public String run(...)这种格式再去算向量让模型一眼看清这是哪个类的哪个方法。CodeIndex 是整个索引流程的入口把“遍历文件 → 分块 → 向量化 → 持久化”封装进去。外面只要一行codeIndex.index(/path/to/project)就能跑起来。遍历用的是 Files.walkFileTreenode_modules、target、.git、build 这些目录直接跳过文件类型也只挑常见的源码后缀。03、Embedding切完块就该生成向量了。EmbeddingClient 支持两种方式。默认走 Ollama 本地模型免费、断网也能跑本地装个 Ollama 拉一个 nomic-embed-text 就能开干。机器扛不动的话再切到远程 API——智谱、阿里千问都 Embedding 模型。切换不用改代码环境变量配一下就行export EMBEDDING_PROVIDERollamaexport EMBEDDING_MODELnomic-embed-text:latestexport EMBEDDING_BASE_URLhttp://localhost:11434切到智谱的话export EMBEDDING_PROVIDERglmexport EMBEDDING_API_KEYyour_key_hereembed 方法按 provider 分发——Ollama 走 /api/embeddingsOpenAI 兼容的走 /embeddings请求体和响应解析在内部处理过了外面只负责传文本拿向量。public float[] embed(String text) throws IOException { String input text.length() MAX_INPUT_CHARS ? text.substring(0, MAX_INPUT_CHARS) : text; return switch (provider.toLowerCase()) { case ollama - embedOllama(input); case openai, zhipu, glm - embedOpenAICompatible(input); default - embedOllama(input); };}MAX_INPUT_CHARS 卡在 2000中文密集的文本大概对应 4000~6000 token喂给 8192 上下文的模型绰绰有余。超出的部分直接截断省得 API 抛错。响应格式两家也不一样。Ollama 把向量放在embedding字段是平铺数组OpenAI 兼容格式塞在data[0].embedding里。在客户端里统一转成float[]上层就不用知道底下是哪一家。HTTP 超时给得比较宽松连接 30 秒、读取 120 秒。Ollama 首次加载模型会比较慢远程 API 一般几秒就回120 秒覆盖两边都足够用。04、向量存储向量存哪这事一开始我纠结了挺久。Milvus、Weaviate 太重了。一个 CLI 工具让用户先起个 docker、配个端口、再装一堆 SDK 才能跑谁顶得住。最后选了 SQLite。向量以 JSON 数组形式塞到 TEXT 字段里检索时全量读到内存逐条算余弦相似度排序取 TopK。可能有小伙伴犯嘀咕全部读到内存能扛得住我自己跑下来——常见的个人项目也就几百到几千个代码块768 维的向量1000 块大约 3MB 内存单次检索几十毫秒。这个量级根本不用上专用向量库等哪天真撑不住了再换也不迟。public ListSearchResult search(float[] queryEmbedding, int topK) throws SQLException { String sql SELECT ... FROM code_chunks WHERE project_path ?; ListSearchResult candidates new ArrayList(); try (PreparedStatement ps connection.prepareStatement(sql)) { ps.setString(1, projectPath); try (ResultSet rs ps.executeQuery()) { while (rs.next()) { float[] embedding jsonToEmbedding(rs.getString(embedding_json)); double similarity cosineSimilarity(queryEmbedding, embedding); candidates.add(new SearchResult(..., similarity)); } } } candidates.sort((a, b) - Double.compare(b.similarity(), a.similarity())); return candidates.size() topK ? new ArrayList(candidates.subList(0, topK)) : candidates;}VectorStore 除了向量检索还顺手把关键词检索和关系图谱查询都做了。关键词走 LIKE专门用来精确命中类名/方法名关系图谱单独一张表存 extends、implements、imports、calls、contains 这五种关系后面查调用链就靠它。批量插入这块加了事务保护。先关掉 autoCommit最后 batch 一把提交中间挂了就 rollback。索引也得老老实实建。project_path、file_path、chunk_type 这几个常用维度都覆盖到关系表的 from_name 和 to_name 也加上。SQLite 再轻量几千条数据上没索引的 LIKE 也能卡死。向量序列化用的是 Jacksonfloat 数组直接转成 JSON 字符串塞到 TEXT 字段。可能有小伙伴想问为啥不用 BLOB理由很简单——JSON 调试方便。打开数据库可视化工具就能看到一行行的向量值定位问题不用再写脚本反序列化。余弦相似度也没调任何第三方库手撸循环点乘除以两个模长几十行就完事。数据库文件默认放在~/.paicli/rag/codebase.db所有项目的索引共用这一个文件靠 project_path 区分。一个项目一个库管起来反而麻烦。要换位置给个paicli.rag.dir系统属性就能改。05、混合检索纯向量检索有个老毛病——对同义词和语义相近的表达很灵敏对精确的代码标识符反而没那么准。比如搜“Agent 的 run 方法”向量检索可能给你返回一堆带“Agent”“run”上下文的块但偏偏不是那个方法。CodeRetriever 的 hybridSearch 干了三件事来补这个短板。第一件语义检索打底。把查询向量化跟库里所有向量算余弦相似度先把语义最近的一批捞出来。第二件关键词加权。用 jieba 把查询切词挑出“Agent”“run”“ReAct”这类代码关键词再用 LIKE 去库里扫一遍。命中的结果按命中位置给不同分——类名/方法名命中 0.3文件路径命中 0.1内容命中 0.1。命中的位置越关键分数越重跟 ES 那一套 BM25 加权思路差不多只是更轻量级。第三件类型加分。method 块 0.15class 块 0.1file 块不加。理由很简单用户问“怎么实现”的时候给方法体比给整个文件有用得多。// 代码类型加分double typeBoost switch (r.chunkType()) { case method - 0.15; case class - 0.10; default - 0.0;};RagQueryTokenizer 也值得拎出来说两句。它用 jieba 做中文分词同时保留 ASCII 标识符类名、方法名里的英文。分块完会过滤掉单字符和“怎么”“如何”“一下”这种没检索价值的停用词。这样一来“用户登录怎么实现”这种自然语言查询“ReAct”“Agent”“MemoryManager”这种纯英文标识符也能保留。还有个小机制叫双重命中奖励。同一个块如果语义检索和关键词检索都命中了额外 0.1。相当于多个维度互相印证分数当然要高一档。这个奖励只给一次不叠加避免某个块因为蹭中好几个关键词就霸榜。最后再加一道同文件去重每个文件最多保留 2 条。不然遇到一个特别大的文件能把整个结果页都占满diversity 就没了。private ListSearchResult limitPerFile(ListSearchResult sorted, int topK, int maxPerFile) { ListSearchResult result new ArrayList(); MapString, Integer fileCount new HashMap(); for (SearchResult r : sorted) { int count fileCount.getOrDefault(r.filePath(), 0); if (count maxPerFile) { result.add(r); fileCount.put(r.filePath(), count 1); if (result.size() topK) break; } } return result;}06、代码关系图谱检索到代码块只是第一步。要真正读懂一个项目得知道“这个类继承了谁、实现了哪些接口、它的方法又调了谁”。CodeAnalyzer 用 JavaParser 做 AST 遍历五种关系一起处理extends类继承、implements接口实现、imports依赖导入只记非 JDK 的、contains类含方法、calls方法调用简化版只记方法名。private void extractClassRelations(String filePath, CompilationUnit cu, ListCodeRelation relations) { cu.findAll(ClassOrInterfaceDeclaration.class).forEach(clazz - { String className clazz.getNameAsString(); // extends 关系 clazz.getExtendedTypes().forEach(ext - { relations.add(new CodeRelation( filePath, className, null, ext.getNameAsString(), extends)); }); // contains 关系类包含方法 clazz.getMethods().forEach(method - { relations.add(new CodeRelation( filePath, className, filePath, className . method.getNameAsString(), contains)); }); // calls 关系方法调用 clazz.findAll(MethodCallExpr.class).forEach(call - { OptionalMethodDeclaration parentMethod findParentMethod(call); if (parentMethod.isPresent()) { String caller className . parentMethod.get().getNameAsString(); relations.add(new CodeRelation( filePath, caller, null, call.getNameAsString(), calls)); } }); });}imports 这块做了过滤只记非 java. 和 javax. 开头的。calls 这块稍微费劲一点。JavaParser 遇到 MethodCallExpr 节点之后得往上回溯找到它属于哪个方法。CodeAnalyzer 里写了个 findParentMethod沿着 AST 父节点一路往上爬碰到 MethodDeclaration 就停。没有这一步只能知道“某个地方调了 chat”但说不清是Agent.run调的还是PlanExecuteAgent.executeTask调的。提取出来的关系全部塞进 SQLite 的 code_relations 表。CLI 里通过 /graph 命令就能查。比如 /graph AgentAgent ├── contains -- Agent.runAgent └── extends -- BaseAgentAgent.run ├── calls -- chatAgent.run ├── calls -- executeTool扫一眼就明白Agent 继承自 BaseAgent挂着一个 run 方法run 内部调了 chat 和 executeTool。07、集成到 AgentRAG 模块写好了但 Agent 不会自己主动用。得在 ToolRegistry 里注册一个 search_code 工具告诉 LLM——遇到代码库相关的问题调这个就行。tools.put(search_code, new Tool( search_code, 语义检索代码库根据自然语言描述查找相关代码块, createParameters( new Param(query, string, 自然语言查询描述例如用户登录的实现, true), new Param(top_k, integer, 返回结果数量默认5, false) ), args - { String query args.get(query); int topK ...; try (CodeRetriever retriever new CodeRetriever(projectPath)) { ListVectorStore.SearchResult results retriever.hybridSearch(query, topK); return SearchResultFormatter.formatForTool(query, results); } }));同时把 Agent 的系统提示词也更新了一下明确告诉 LLM如果用户询问与代码库相关的问题如“这个类是干什么的”、“哪里用了某个功能”请优先使用 search_code 工具检索相关代码再基于检索结果回答。这里有个特别容易翻车的细节——ToolRegistry 里的 projectPath 默认取 user.dir但用户可能用 /index 索引了另一个目录。如果不同步工具检索的还是老路径搜出来都是空的。所以 Main 里 /index 执行完之后会立刻把索引路径同步给 ToolRegistry确保两边对齐。SearchResultFormatter 把搜索结果整理成人能看的样子。它有两个输出模式formatForCli 给命令行用带 emoji 和缩进formatForTool 给 LLM 用更紧凑包含一段搜索摘要加带行号的代码片段。摘要里会告诉模型“最相关的入口是哪个方法”“结果主要集中在哪些文件”“排序综合参考了哪些关键词”让 LLM 能快速判断该重点看哪几条。PlanExecuteAgent 的执行提示词也加了类似的引导规划任务里凡是涉及代码理解的步骤都会自动触发检索。这样不管是 ReAct 还是 Plan-and-Execute两种模式都能看懂代码库。08、CLI 实战RAG 这一堆功能最后通过三条 CLI 命令交到用户手上。第一条是/index给代码库建索引。执行的时候它会遍历项目目录node_modules、target、.git 这些自动跳过每个文件分块、向量化、塞 SQLite。第二条是/search用自然语言检索代码。不用记类名、不用记文件名跟问同事差不多。第三条是/graph查类或方法的关系图谱。 你: /graph Agent️ 查询类关系图谱: Agent 找到 5 条关系: Agent ├── contains -- Agent.run Agent ├── contains -- Agent.clearHistory Agent └── extends -- BaseAgent Agent.run ├── calls -- chat Agent.run ├── calls -- executeTool/index也支持指定路径比如/index /Users/xxx/my-project可以索引任意目录的代码库。索引过程每 10 个文件打一次进度单个文件解析失败只会打 warn不会中断整体流程。最后再输出一行统计——多少代码块、多少条关系一眼就知道这次索引的质量怎么样。代码库更新了怎么办重新执行 /index 就行。CodeIndex 会先清掉旧数据再写新数据保证向量库和代码库始终对齐不会出现“代码已经改了搜出来还是老版本”那种灵异现象。/search在没建索引的情况下会友好提示“代码库尚未索引请先使用/index命令”不会直接抛异常糊脸。检索过程出错也会捕获异常打日志CLI 不会直接崩掉。日常用法基本就是先/index一次建索引平时有问题就/search自然语言搜一下想看架构就/graph查关系。Agent 模式更省事问题直接抛给它背后自动调 search_code连/search都不用手动敲。pom.xml 里这一期新增了三个依赖sqlite-jdbc 管向量持久化javaparser-core 管 AST 解析jieba-analysis 管中文分词。加上前几期已经有的 jackson-databind 和 okhttp整个 RAG 模块外部依赖控制在 5 个以内跟之前的轻量定位保持一致。ending四期下来PaiCLI 从一个只会一步步走的 ReAct Agent逐渐进化到能规划、能记忆、还能读代码库的完整工具。代码全部开源在 GitHub 上第四期新增了 10 个 RAG 相关的类累计代码量到了 1700 行左右。跟着教程敲一遍本地就能跑起来一个属于自己的 Agent CLI。如果用着用着发现检索不准大概率是索引粒度或者查询分词的问题。可以先试试调整 MAX_CHUNK_CHARS 的大小或者给 RagQueryTokenizer 加几个跟业务相关的停用词多数场景下都能见效。下期预告——Multi-Agent 协作。一个 Agent 忙不过来咱就搞个团队规划的规划、执行的执行、检查的检查分工干。简历包装PaiCLI 项目项目名称PaiCLI —— 开源 Java Agent CLI 工具项目简介从零构建一个类 Claude Code 的命令行 Agent 工具支持 ReAct 推理、Plan-and-Execute 任务规划、Memory 记忆系统、RAG 代码库检索。技术栈Java 17、JavaParser、SQLite、Jieba 分词、OkHttp、Jackson、JUnit 5、JLine3核心职责基于 JavaParser AST 实现代码多粒度分块文件/类/方法非 Java 文件按大小分段检索召回显著提升统一封装 Ollama 本地模型和 OpenAI 兼容远程 API通过环境变量丝滑切换 provider支持文本自动截断防止 API 超限基于 SQLite 实现轻量级向量存储向量以 JSON 数组持久化通过在内存中计算余弦相似度单项目千行级代码块检索耗时 100ms实现混合检索策略语义检索打底 jieba 分词加权 代码类型加分 同文件限流Top5 准确率达到可用生产级别基于 AST 提取代码关系图谱extends/implements/imports/calls/contains支持通过自然语言查询类的调用链将 RAG 封装为 search_code 工具注册到 Agent 工具通过 LLM 系统提示词引导自动触发检索ReAct 和 Plan-and-Execute 双模式均支持代码库理解学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 学习资料已经上传CSDN朋友们如果需要可以微信扫描下方CSDN官方认证二维码免费领取【保证100%免费】