# tinyChatMind **Repository Path**: wkling/tiny-chat-mind ## Basic Information - **Project Name**: tinyChatMind - **Description**: 基于SpringAI的单agent系统、采用think_exec循环,支持agent创建、RAG检索等功能。 - **Primary Language**: Unknown - **License**: Not specified - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2026-03-01 - **Last Updated**: 2026-03-04 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # tinyChatMind项目学习 ## 相关技术栈 - 后端:JDK17、SpringAI - 数据库:PostgreSQL18 - Embedding:Ollama,bge-m3 - 前端:NodeJS22,vibe coding出的前端页面 ## 数据模型 项目涉及到七张表,6张传统的SQL表与一个chunk_bge_m3向量数据表。 - agent表,主要存每个创建的agent的信息,包括agentName、系统提示词、关联的模型、允许使用的工具、关联的知识库、温度、top_p等配置。 - chat_session表,会话记录表,session_id、存会话title、绑定的agent - chat_message表,session_id、role和content、额外的metadata如模型参数、RAG等 - knowleage_base表, - document表, - chunk_bge_m3表,存储向量化的RAG数据,基于标题-内容拆分文档内容,embedding字段存标题向量、content字段存内容。 ## SpringAI框架 **自己总结的几个特点**: - SpringAI天然支持SpringBoot,作为框架,开箱即用,能帮人快速搭建一个agent。 - AI Agent后端设计的最重要最复杂的部分:模型维护、上下文管理(消息组织与上下文压缩),不同模型的底层通信协议实现,框架都帮我们做好了(通过ChatCilent对象和ChatMemory对象)。 - SpringAI也提供自动工具执行功能和自动工具调用决策(本项目是自己管理工具决策),同时,通过`@Tool`注解,就能把Java方法注册为工具,供`toolCallingManager`自动选择调用 SpringAI框架结合不同模型厂商的starter依赖,提供了对不同模型的封装,如下: ``` org.springframework.ai spring-ai-starter-model-deepseek ``` 在application.yaml中配置好参数后,就可以被SpringAI使用。 一个模型,即为一个`chatclinet`对象,它屏蔽了底层调用细节,只暴露统一的对话接口。 ```Java @configuration public class MultiChatclientconfig { @Bean( "deepseek-chat") public chatclient deepseekchatclient(DeepSeekchatModel model) { return Chatclient.create ( model); } @Bean( "glm-4.6") public chatclient zhiPuchatclient(zhiPuAiChatModel model) { return chatclient.create( model); } } ``` Spring 会自动把所有Chatclient类型的Bean注入到一个Map中,Key为 Bean名称,Value为`chatclient`实例本身。 实现了模型注册。 ```Java @component public class ChatclientRegistry { private final Map registry; public ChatclientRegistry(Map registry) { this.registry = registry; } public chatclient get( string modelName) { return registry.get( modelName ) ; } } ``` ## Think-Execute循环流程 创建agent实例、调用`.chat`、执行think-execute流程,for循环中发送prompt让LLM决策是否需要调用工具,若需要则处理工具调用;若不需要则结束think-execute、处理工具调用后的结果,加入`chatMemory`中,作为会话记忆的一部分。 ```Java // 1. 创建 V2 实例(可改用 Builder 模式) JChatMindV2 agent = new JChatMindV2( "test-agent-v2", "测试 Agent V2", "你是一个智能助手...", chatClient, 20, "test-session-v2", Arrays.asList(toolCallbacks) ); // 2. 执行循环 for (int i = 0; i < MAX_STEPS && agentState != AgentState.FINISHED; i++) { step(); if (i >= MAX_STEPS - 1) { agentState = AgentState.FINISHED; log.warn("达到最大步骤数"); } } // 3. 步骤:think 或 finish protected void step() { if (think()) { execute(); // 有工具则执行 } else { agentState = AgentState.FINISHED; // 无工具则结束 } } // 4. 思考:LLM 决策 protected boolean think() { this.lastChatResponse = chatClient .prompt(Prompt.builder() .chatOptions(this.chatOptions) .messages(chatMemory.get(sessionId)) .build()) .system("你是决策模块,根据上下文决定是否调用工具。") .toolCallbacks(availableTools != null ? availableTools.toArray(new ToolCallback[0]) : new ToolCallback[0]) .call() .chatClientResponse() .chatResponse(); List toolCalls = this.lastChatResponse .getResult() .getOutput() .getToolCalls(); // 无工具调用时,将回复加入记忆 if (toolCalls.isEmpty()) { chatMemory.add(sessionId, this.lastChatResponse.getResult().getOutput()); } return !toolCalls.isEmpty(); // 返回是否需要执行工具 } ``` 最后的最后,工具的具体执行,也是交由SpringAI框架做的。 ```Java private void execute() { // 手动调用 ToolCallingManager 执行工具 ToolExecutionResult toolExecutionResult = toolCallingManager.executeToolCalls( prompt, this.lastChatResponse ); // Spring AI 内部做的事情: // 1. 解析 lastChatResponse 中的 ToolCall(工具名、参数) // 2. 找到对应的 Java 方法(通过 @Tool 注解) // 3. 反射调用方法,传入参数 // 4. 获取返回值 // 5. 封装成 ToolResponseMessage } ``` ## 知识库与RAG RAG能力在本项目中,是以工具的形式提供的。 流程:用户提出问题 —> Agent判断:我是否需要查阅知识库 —> Agent调用KnowledgeTool —> 系统执行向量检索 —> 返回相关文档内容 —> Agent基于内容生成最终回答。 目前只支持markdown文本。 ### PART1:内容放进知识库 内容向量化:文档分割成为一个一个chunk、送入向量模型embedding向量化,结果存入postgreSQL内。 怎么拆文档:分割chunk的方式有很多种,例如固定长度分割、按照段落分割、本项目基于markdown章节标题拆。**提取出章节标题,把标题向量化,chunk_bge_m3表中的embedding字段存标题向量、content字段存内容。** 解析markdown文档,基于AST方式解析: ```Java // 基于flexmark解析器 @Override public List parseMarkdown(InputStream inputStream) { try { // 读取文件内容 originalMarkdownContent = new String(inputStream.readAllBytes(), StandardCharsets.UTF_8); // 解析 Markdown Document document = parser.parse(originalMarkdownContent); // 提取标题和内容 List sections = new ArrayList<>(); extractSections(document, sections); log.info("解析 Markdown 完成,共提取 {} 个章节", sections.size()); return sections; } catch (Exception e) { log.error("解析 Markdown 失败", e); throw new RuntimeException("解析 Markdown 失败: " + e.getMessage(), e); } } ``` 向量化title,并存入数据库。 ```Java for (MarkdownParserService.MarkdownSection section : sections) { String title = section.getTitle(); String content = section.getContent(); if (title == null || title.trim().isEmpty()) { continue; } // 对标题进行 embedding float[] embedding = ragService.embed(title); // 创建 ChunkBgeM3 实体 ChunkBgeM3 chunk = ChunkBgeM3.builder() .kbId(kbId) .docId(documentId) .content(content != null ? content : "") .metadata(null) // 可以存储标题信息到 metadata .embedding(embedding) .createdAt(now) .updatedAt(now) .build(); // 插入数据库 int result = chunkBgeM3Mapper.insert(chunk); ``` 文本向量化流程: ```Java private float[] doEmbed(String text) { EmbeddingResponse resp = webClient.post() .uri("/api/embeddings") .bodyValue(Map.of( "model", "bge-m3", "prompt", text )) .retrieve() .bodyToMono(EmbeddingResponse.class) .block(); Assert.notNull(resp, "Embedding response cannot be null"); return resp.getEmbedding(); } ``` 1. 调用本地 Ollama 服务:地址: http://localhost:11434、端点: /api/embeddings 2. 发送请求: ``` { "model": "bge-m3", // 使用 BGE-M3 嵌入模型 "prompt": "文本内容" // 待向量化的文本 } ``` 3. 接收响应 `{"embedding": [0.123, -0.456, 0.789, ...] // 1024 维向量}`,返回 float[] 向量数组 最终的向量并没有被存入某个独立的向量数据库,而是直接使用PostgreSQL + pgvector:`embedding VECTOR(1024) NOT NULL` 这样做的原因并不复杂: - 不需要引入额外的基础设施。 - 向量数据和业务数据在同一数据库中·查询路径清晰、可调试、可观测。 - 通过pgvector提供的<->操作符,我们可以非常直接地完成Top-K相似度搜索。 ### PART2:知识库检索内容 - 用户提问(往往缺少背景信息)。对用户问题进行Embedding。 - 使用用户问题的向量在向量数据库中执行相似度检索。 - 检索到的 Top-K条chunk,会被作为补充上下文,与用户原始问题一起发送给大模型·模型基于用户问题和检索到的知识片段生成最终回答并返回给用户。 **—次完整的相似度搜索流程如下:** 1. 将查询文本生成Embedding 2. 使用该向量在指定知识库范围内执行相似度排序 3. 返回最相关的若干个章节内容 4. 对应的SQL查询也非常直观: ```SQL