tinyChatMind:基于SpringAI的单agent系统

相关技术栈

  • 后端:JDK17、SpringAI
  • 数据库:PostgreSQL18
  • Embedding:Ollama,bge-m3
  • 前端:NodeJS22,vibe coding出的前端页面

代码:yuyuyuyi1/tinyChatMind

数据模型

项目涉及到七张表,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依赖,提供了对不同模型的封装,如下:

<dependencies>
    <dependency>
    <groupId>org.springframework.ai</groupId>
    <artifactId>spring-ai-starter-model-deepseek</ artifactId></dependency>
    <dependency>
</dependencies>

在application.yaml中配置好参数后,就可以被SpringAI使用。

一个模型,即为一个chatclinet对象,它屏蔽了底层调用细节,只暴露统一的对话接口。

@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实例本身。 实现了模型注册。

@component
public class ChatclientRegistry {
private final Map<string,chatclient> registry;
    public ChatclientRegistry(Map<string,chatclient> 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中,作为会话记忆的一部分。

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<AssistantMessage.ToolCall> toolCalls = this.lastChatResponse
        .getResult()
        .getOutput()
        .getToolCalls();
​
    // 无工具调用时,将回复加入记忆
    if (toolCalls.isEmpty()) {
        chatMemory.add(sessionId, this.lastChatResponse.getResult().getOutput());
    }
​
    return !toolCalls.isEmpty();  // 返回是否需要执行工具
}

最后的最后,工具的具体执行,也是交由SpringAI框架做的。

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方式解析:

// 基于flexmark解析器
    @Override
    public List<MarkdownSection> parseMarkdown(InputStream inputStream) {
        try {
            // 读取文件内容
            originalMarkdownContent = new String(inputStream.readAllBytes(), StandardCharsets.UTF_8);
            // 解析 Markdown
            Document document = parser.parse(originalMarkdownContent);
            // 提取标题和内容
            List<MarkdownSection> 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,并存入数据库。

   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);

文本向量化流程:

    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查询也非常直观:
<select id="similaritysearch" resultMap="BaseResultMap">
  <! [CDATA[
   SELECT id,
  kb_id,doc_id,content,metadata,embedding,created_at,updated_at
   FROM chunk_bge_m3
   WHERE kb_id = CAST(#{ kbId} AS uuid)
   ORDER BY embedding <->#{ vectorLiteral} : : vectorLIMIT #{limit}
  ]]>
</ select>

RAG在本项目中,作为一个可调用的工具,提供给LLM(通过提示词,告诉LLM可用哪些知识库,知识库的大致用途),让LLM来判断是否要用知识库。

最后:整体的流程架构

整体的前后端流程,大概可描述如下:

  1. 前端用户发送请求,后端先持久化请求到数据库,发布事件,并返回给前端。
  2. 后端有一个ChatEven事件监听器,事件监听器来真正处理Agent调用流程。事件监听器是一个异步任务,这意味着无论Agent跑多久,无论中途调用多少功能,都不会影响用户继续操作页面,监听器拿到事件之后,只做一件事:创建个Agent实例,并调用run()。
  3. Agent的流程,已经在Think-Execute循环流程中介绍过。
  4. 消息不是立即推送的,相较于completeAPI协议的LLM每delta都推送,本项目在运行过程中,并不会每生成一条消息就立刻推送,而是先统一保存到pendingChatMessages,每次生成一条完整消息的时候,再一次性推送。

问题

  • tinyChatMind项目中,设计了AI的几个状态,用途是为什么,对比实习的项目,实习的项目为什么没有状态?
    IDLE,  // 空闲
    PLANNING,  // 计划中
    THINKING,  // 思考中
    EXECUTING, // 执行中
    FINISHED,  // 正常结束
    ERROR  // 错误结束
  • tinyChatMind项目,对比实习的项目,缺陷的地方:
    • 工具调用,实习项目是基于标签解析的,tinyChatMind交由SpringAI
    • 上下文管理,实习项目自己组装历史消息,进行上下文压缩,设计底层通信协议,
    • 模型管理,实习项目基于微服务配置+模型池管理,tinyChatMind通过注册表模式管理。
    • 待补充…
暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇