deepseek的问答功能是怎么做的----我的基于spring ai的设计
项目地址:http://72.249.203.242/
账号/密码: test/test
源码地址:http://github.com/zyz-hu/ai-weblog-continued-project
建表
思考一下需要持久化哪些数据,e-r图要先明确,表也要先明确
首先我的考虑是建一个对话主表,一个对话详细信息表
对话主表主要存一个对话的元信息,对话详细信息表主要存每个对话的详细信息,一个对话主表行对应多个对话详细信息行,一个对话详细信息行对应一个对话主表行.
对话主表必须有这些属性,其他的可以后续拓展,基础字段不提了:
对话标题
用户id
对话唯一标识(这个可以考虑使用id,或者新建一列,都是可以的,从我的角度来说.如果不分库分表,直接用id就行,如果需要分库分表,可以考虑用分布式id生成服务生成分布式id,作为id存储)
对话详细信息表必需的字段:
唯一标识
对话主表唯一标识
角色(一般三个,用户角色,系统角色,助手角色)
对话文本
深度思考过程文本
对应模型
扩展元数据(存联网搜索涉及到的一些拓展的元数据)
表准备好可以开始写问答机器人的功能了,这里我不想过多赘述一些查询对话列表.对话详细信息等crud接口,直接来说最核心的对话接口如何实现
对话接口如何实现
首先考虑这个接口的思路是什么和一个大致的请求响应链路
然后考虑使用什么设计模式,什么细节去实现这个逻辑
实现,并补充细节
最后在不影响设计的情况下修bug
这个接口的思路是什么
这个接口其实需要细分为三层的处理,每层对应一个功能的处理
我将之称为:
模型选择层
请求增强层
流式返回层
一个大致的请求响应链路是怎样的
前端输入消息
->
前端 sendMessage()
->
fetchEventSource 调用 对话接口
->
传递参数对话id,用户输入的信息message,模型名字,是否联网搜索,温度值(这些字段是我实现这个需求必需要传递的,我一开始其实也没传递全,是在开发过程中一点点补齐的,所以不用纠细节,为什么是这几个字段.需求用到了,所以我加了这些字段,就是这样)
->
参数校验 / 会话归属校验
->
AIModelFactory 选择模型策略(这个具体架构下文展开,也是因为需求和模型文档调研之后,用的这个架构,最开始其实很简单,一个条件判断就可以,后续才考虑的架构调整)
->
组装 Advisor 链(这个是增强对话功能的分支,下文展开)
->
strategy.streamResponse()(请求模型,得到流式响应数据)
->
模型流式返回 chunk
->
后端把 chunk 转成统一 AIResponse(把模型返回的数据转为我自己定义的一个响应类对象)
->
合并 heartbeatStream(这个是心跳流,这个我一开始是没加的,由于最后调试的时候发现了一些问题,所以加了这个,下文细说)
->
SSE 持续返回前端(这个主要就是使用sse协议流式返回数据给前端)
->
前端按 type 分流展示 reasoning / content / ping
接下来顺序展开一下这个链路:
AIModelFactory 选择模型策略
这是个啥
其实就是建了一个工厂类:这个类中有个方法可以根据模型名字,返回一个策略实现类的Bean对象
代码:
@Component
public class AIModelFactory {
private final List<AIModelStrategy> strategies;
// Spring 会自动注入所有实现了 AIModelStrategy 的 Bean 到这个 List 中
public AIModelFactory(List<AIModelStrategy> strategies) {
this.strategies = strategies;
}
/**
* 获取对应的策略
*/
public AIModelStrategy getStrategy(String modelName) {
return strategies.stream()
.filter(strategy -> strategy.supports(modelName))
.findFirst()
.orElseThrow(() -> new IllegalArgumentException("不支持或未配置的 AI 模型: " + modelName));
}
}
public interface AIModelStrategy {
boolean supports(String modelName);
ChatClient.ChatClientRequestSpec createRequest(String modelName, String userMessage, Double temperature);
/**
* 获取该模型对应的思考提取器
* @return 提取器实例
*/
ReasoningExtractor getReasoningExtractor();
/**
* 默认实现:使用 ChatClient 开启流式输出。
* 特殊模型(如官方 SDK 调用)可覆盖此方法自定义流式逻辑。
*/
default Flux<AIResponse> streamResponse(String modelName, String userMessage, Double temperature, List<Advisor> advisors) {
ChatClient.ChatClientRequestSpec requestSpec = createRequest(modelName, userMessage, temperature);
if (advisors != null && !advisors.isEmpty()) {
requestSpec.advisors(advisors);
}
return requestSpec
.stream()
.chatResponse()
.mapNotNull(this::convertResponse);
}
/**
* 将底层的 ChatResponse 转换为前端需要的 VO
* 支持不同模型返回不同结构数据的逻辑解耦
*/
AIResponse convertResponse(ChatResponse response);
}
为啥要这么做
其实条件分支也可以实现我想要的模型选择,根据模型名字进入不同分支就行
但这会有一个问题:不同的模型,他们想要实现同一种功能,具体到实现层面,是个各有不同的,所以可以预见的是,如果我用了条件分支,我每加一个模型.都要写一套实现功能的特定的方法,还要加条件分支,这些方法可以预见的会越积越多,导致类越来越混乱,此外,加模型条件分支还要动业务类的代码,这些这些缺点足以pass条件分支,而工厂加策略可以完美解决这个问题,所以这里用了策略类.
组装 Advisor 链展开
这个干嘛的
除了对话功能,上游或者下游需要实现一些额外的功能,或者需要在上游和下游需要对输入或者输出做一些操作,统称为:想要对对话功能做增强,所以用了advisor
我做了哪些增强
主干:
用户发消息
-> chat 接口
-> 选择模型
-> 调模型
-> 流式返回
增强分支 A:
联网搜索增强
-> 搜索
-> 抓正文
-> 改写 Prompt
增强分支 B:
聊天记忆增强
-> 查历史消息
-> 拼到上下文
增强分支 C:
日志与持久化增强
-> 聚合 chunk
-> 提取 reasoning
-> 存消息 / 元数据
其实本质上这些增强主要是我想实现这些需求:
问模型前:
先补搜索结果
先补聊天历史
问模型后:
把结果记录下来
把 reasoning 提取出来
合并 heartbeatStream 这个在解决啥
最后讲讲合并 heartbeatStream这个在解决啥:
没加这个的时候,有些时候模型处理输入有些慢,联网搜索也需要时间,导致sse连接由于长时间没有传递数据,会自动断开,结果就是前端生成报错,其实这不是错误,只是因为连接超时了,所以就考虑加了个心跳,如果对话线程没有处理完任务,心跳线程隔一段时间就ping一次前端,维持这个sse连接)
差不多就讲这么多,其实advise链增强我还没细说,下次写博客再说吧
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐



所有评论(0)