# 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 artifactId>
```
在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