尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

AI陪伴机器人技术选型:SpringBoot3+LangChain4j+MySQL实战

AI陪伴机器人技术选型:SpringBoot3+LangChain4j+MySQL实战 做AI陪伴机器人这个方向技术选型这件事我前前后后推翻过三套方案。最早想用Python全家桶FastAPI加LangChain再加PostgreSQL跑Demo确实快但一到要交付给团队做长期迭代就各种别扭——前端同学不熟Python生态运维同学对Gunicorn加Celery的部署方式有意见最要命的是招人时发现既懂LangChain又愿意写Java业务代码的人实在太少。后来试过Node.js那一套TypeScript加LangChain.js类型是舒服了但企业级的事务管理、连接池、监控埋点又得重新造轮子。折腾了小半年最终落地的组合是SpringBoot3加LangChain4j加MySQL。这篇文章就把这本账从头到尾摊开算一遍为什么是这三个而不是别的。1. 先把需求摊开AI陪伴机器人到底在跑什么选型这件事最怕的就是脱离场景谈技术。陪伴机器人和那种一问一答的客服机器人完全不是一回事它的技术画像有几个非常鲜明的特征这些特征直接决定了后面所有的选型判断。1.1 陪伴场景对系统的三个硬性要求第一个硬性要求是长会话状态要保持。陪伴机器人不是每次对话都从零开始它得记得用户上周说过自己养了只猫记得用户最近在准备考试压力大记得用户不喜欢被叫亲。这些记忆有的存在会话上下文里有的得落到数据库里长期保存。这就意味着系统必须有一个可靠的状态管理层而不是把历史消息往内存里一塞就完事。第二个硬性要求是多用户并发下的资源隔离。一个陪伴机器人产品上线后同时在线几百上千个会话是常态。每个会话都在调大模型、都在读写记忆、都在做RAG检索。如果资源不隔离一个用户的超长上下文把线程池占满其他用户全部卡死。这就要求底层框架必须有成熟的线程池管理、连接池管理和限流降级能力。第三个硬性要求是业务逻辑的复杂度会持续膨胀。陪伴机器人一开始可能只是聊天后面会加情感分析、加主动关怀推送、加语音交互、加多模态理解、加会员体系、加内容审核。这些全是标准的业务系统需求需要事务、需要定时任务、需要权限控制、需要审计日志。用一套纯AI框架去扛这些迟早要重构。1.2 为什么能跑Demo和能上线是两码事我见过太多团队拿着一个Jupyter Notebook里的LangChain Demo就敢说我们的AI能力已经就绪。Demo和上线的差距在陪伴机器人这个场景里被放得特别大。Demo阶段你只需要考虑输入一句话输出一句话上线阶段你要考虑的是这条消息的token消耗记在哪个用户头上、这次模型调用超时了要不要重试、重试会不会导致用户收到两条回复、用户的记忆数据存在哪张表、这张表的索引怎么建、查询慢查询日志里出现这条SQL怎么办。这些问题没有一个是LangChain本身能回答的它们全是工程问题。而工程问题恰恰是SpringBoot生态最擅长的事情。所以选型的核心逻辑不是哪个AI框架最强而是哪个组合能让AI能力和工程能力都不掉链子。1.3 三个组件各自要扛的活把职责分清楚后面的选型讨论才有依据。在我的方案里三个组件的分工是这样的组件承担的职责不负责的部分SpringBoot3业务编排、事务管理、连接池、定时任务、监控埋点、接口暴露不直接管大模型调用细节LangChain4j大模型调用、Prompt模板、RAG检索链、记忆管理、工具调用不管业务事务和持久化MySQL用户数据、会话历史、长期记忆、向量检索配合扩展、业务数据不做实时计算和缓存这个分工的关键在于边界清晰。LangChain4j只做它擅长的事——把大模型能力封装成Java对象SpringBoot3做它擅长的事——把一切编排成可靠的服务MySQL做它擅长的事——把数据稳稳当当地存下来。三者之间通过明确的接口交互任何一方出问题都不会把整个系统拖垮。2. SpringBoot3不是因为它流行是因为它扛得住很多人选SpringBoot是因为大家都在用这个理由在陪伴机器人项目里其实站不住脚。我选它是因为它在几个关键维度上给出了别的框架给不了的东西。2.1 虚拟线程陪伴机器人并发的真正解法SpringBoot3最大的一个变化是支持了虚拟线程Virtual Threads。这个东西对陪伴机器人来说简直是量身定做的。陪伴机器人的请求模式是什么是大量IO等待。等大模型返回、等数据库查询、等RAG检索、等语音合成。每个请求的CPU计算时间可能只有几毫秒但IO等待时间动辄几秒。传统的平台线程模型下一个线程处理一个请求线程在IO等待时被阻塞但线程本身占着内存。假设每个线程栈1MB1000个并发就是1GB内存还没算业务对象。而虚拟线程的栈是动态的初始只有几百字节可以轻松支撑几十万个并发。这意味着什么意味着你不需要为了扛并发去搞复杂的响应式编程用最直观的同步阻塞写法就能达到响应式的吞吐量。我实测过一组数据在同样的4核8G机器上处理1000个并发的对话请求每个请求模拟2秒的IO等待线程模型平均响应时间最大并发内存占用平台线程200线程池10.2秒200约1.8GB虚拟线程2.1秒1000约600MB这个差距在陪伴机器人场景里是决定性的。用户不会接受等10秒才收到回复但2秒是可以接受的。2.2 事务管理记忆写入不能丢陪伴机器人有一个很容易被忽视的需求记忆写入的原子性。用户说了一句话系统要同时做几件事把消息存进会话历史表、更新用户的长期记忆摘要、扣减token配额、记录调用日志。这四件事必须一起成功或者一起失败否则就会出现消息存了但配额没扣或者记忆更新了但历史没存的脏数据。SpringBoot的声明式事务在这个场景下就是救命稻草。一个Transactional注解配合MySQL的InnoDB引擎就能保证这四步操作的原子性。如果用Python的LangChain生态你得自己处理事务边界自己管理连接自己处理回滚稍不注意就出问题。这不是说Python做不到而是说SpringBoot把它变成了默认行为你不需要额外操心。Transactional(rollbackFor Exception.class) public void processUserMessage(Long userId, String message) { // 1. 保存会话历史 chatHistoryRepository.save(buildHistory(userId, message)); // 2. 更新长期记忆 memoryService.updateLongTermMemory(userId, message); // 3. 扣减配额 quotaService.deduct(userId, estimateTokens(message)); // 4. 记录日志 callLogRepository.save(buildLog(userId, message)); // 任何一步抛异常全部回滚 }2.3 连接池与监控上线后才懂的价值HikariCP是SpringBoot默认的数据库连接池它的性能在业界是公认的第一梯队。陪伴机器人对数据库的访问模式是高频短查询连接池的配置直接决定了数据库层的吞吐。我一般会把maximumPoolSize设成CPU核数的2到4倍connectionTimeout设成3秒idleTimeout设成10分钟。这些参数不是拍脑袋定的是根据陪伴机器人的查询特征调出来的。监控这块SpringBoot Actuator加上Micrometer可以无缝对接Prometheus和Grafana。上线后我最关心的几个指标是大模型调用的P99延迟、数据库连接池的活跃连接数、虚拟线程的创建速率、每个用户的token消耗速率。这些指标在SpringBoot生态里都是开箱即用的不需要自己写埋点代码。提示虚拟线程虽然好用但不要和synchronized块混用。虚拟线程在synchronized块里会被钉在平台线程上pinning失去虚拟线程的优势。陪伴机器人里如果有需要同步的代码改用ReentrantLock。3. LangChain4jJava生态里唯一能打的AI编排框架LangChain4j这个框架我第一次看到的时候是持怀疑态度的。Java生态做AI框架历史上就没几个成功的。但用下来发现它解决了一个非常具体的问题让Java开发者用自己熟悉的方式调用大模型。3.1 为什么不用Python的LangChain这个问题我被问过无数次。答案很简单你的业务系统是什么语言写的。如果你的陪伴机器人后端是Java写的你引入Python的LangChain就意味着要维护两套运行时、两套依赖管理、两套部署流程、两套监控体系。跨语言调用的序列化开销、网络开销、错误处理复杂度在陪伴机器人这种高频调用场景下会被放大到不可接受。LangChain4j的API设计思路和Python版LangChain很像但它是纯Java的可以直接在你的SpringBoot应用里用。你不需要起一个Python服务不需要写gRPC接口不需要处理跨语言的异常映射。一个Maven依赖就搞定dependency groupIddev.langchain4j/groupId artifactIdlangchain4j-open-ai/artifactId version0.35.0/version /dependency dependency groupIddev.langchain4j/groupId artifactIdlangchain4j-spring-boot-starter/artifactId version0.35.0/version /dependency3.2 记忆管理LangChain4j最被低估的能力陪伴机器人最核心的能力是记得住。LangChain4j的ChatMemory抽象是我见过最实用的记忆管理设计。它把记忆分成两层短期记忆当前会话的上下文窗口和长期记忆跨会话的持久化记忆。短期记忆用MessageWindowChatMemory它维护一个固定大小的消息窗口超出窗口的旧消息自动淘汰。这个窗口大小需要根据模型上下文长度和成本来权衡。我一般设成20条消息因为陪伴机器人的对话轮次通常不会太长20条足够覆盖当前话题的上下文。长期记忆的落地方式更值得说。LangChain4j提供了EmbeddingStore接口可以把记忆向量化后存起来。但这里有个坑LangChain和LangChain4j的默认RRFReciprocal Rank Fusion实现去重逻辑存在缺陷。如果你同时用向量检索和关键词检索然后做RRF融合可能会得到重复的结果。我在项目里是自己重写了去重逻辑在融合前先按文档ID去重再算RRF分数。public ListEmbeddingMatchTextSegment hybridSearch(String query, int maxResults) { // 向量检索 ListEmbeddingMatchTextSegment vectorResults vectorStore.search( EmbeddingSearchRequest.builder() .queryEmbedding(embeddingModel.embed(query).content()) .maxResults(maxResults * 2) .build() ).matches(); // 关键词检索用MySQL全文索引 ListTextSegment keywordResults keywordSearch(query, maxResults * 2); // 先按ID去重再做RRF融合 MapString, Double scoreMap new HashMap(); MapString, TextSegment segmentMap new HashMap(); for (int i 0; i vectorResults.size(); i) { String id vectorResults.get(i).embeddingId(); segmentMap.putIfAbsent(id, vectorResults.get(i).embedded()); scoreMap.merge(id, 1.0 / (60 i 1), Double::sum); } for (int i 0; i keywordResults.size(); i) { String id keywordResults.get(i).metadata().getString(id); segmentMap.putIfAbsent(id, keywordResults.get(i)); scoreMap.merge(id, 1.0 / (60 i 1), Double::sum); } return scoreMap.entrySet().stream() .sorted(Map.Entry.String, DoublecomparingByValue().reversed()) .limit(maxResults) .map(e - new EmbeddingMatch(e.getValue(), e.getKey(), null, segmentMap.get(e.getKey()))) .collect(Collectors.toList()); }3.3 RAG在陪伴机器人里的正确用法RAG检索增强生成在陪伴机器人里的用法和知识库问答完全不一样。知识库问答是用户问什么我检索什么陪伴机器人是用户说什么我检索相关的记忆和背景知识。这个区别决定了检索策略的不同。陪伴机器人的RAG检索有三个来源用户的历史记忆、用户的画像标签、通用的陪伴话术库。这三个来源的检索权重不一样。历史记忆权重最高因为它最个性化画像标签次之它决定了回复的风格通用话术库权重最低它只是兜底。我在实现的时候把这三个来源分别做了检索然后在Prompt组装阶段按权重拼接。LangChain4j的RetrievalAugmentor接口可以自定义这个逻辑但默认实现不够灵活我建议自己实现一个。public class CompanionRetrievalAugmentor implements RetrievalAugmentor { Override public AugmentationResult augment(AugmentationRequest request) { String query request.chatMessage().singleText(); Long userId extractUserId(request); // 三路检索 ListTextSegment memories memoryStore.search(userId, query, 5); ListTextSegment profile profileStore.getProfile(userId); ListTextSegment scripts scriptStore.search(query, 3); // 按权重组装 StringBuilder context new StringBuilder(); context.append(【用户记忆】\n); memories.forEach(m - context.append(m.text()).append(\n)); context.append(\n【用户画像】\n); profile.forEach(p - context.append(p.text()).append(\n)); context.append(\n【参考话术】\n); scripts.forEach(s - context.append(s.text()).append(\n)); return AugmentationResult.builder() .chatMessage(new SystemMessage(context.toString())) .build(); } }3.4 工具调用让机器人真的能做事陪伴机器人如果只会聊天价值是有限的。LangChain4j的Tool注解让Java方法可以直接暴露给大模型调用。比如用户说帮我记一下明天下午三点要开会机器人可以调用一个createReminder方法把提醒存进数据库。public class CompanionTools { Tool(创建提醒事项) public String createReminder( P(提醒内容) String content, P(提醒时间格式yyyy-MM-dd HH:mm) String time ) { // 存库逻辑 reminderService.create(content, LocalDateTime.parse(time, DateTimeFormatter.ofPattern(yyyy-MM-dd HH:mm))); return 提醒已创建 content 时间 time; } Tool(查询用户最近的提醒) public String queryReminders(P(用户ID) Long userId) { ListReminder reminders reminderService.findRecent(userId); return reminders.stream() .map(r - r.getTime() r.getContent()) .collect(Collectors.joining(\n)); } }这里有个实操经验工具方法的描述要写得像给同事解释一样清楚。大模型是根据方法描述和参数描述来决定调不调、怎么调的。描述写得含糊模型就会乱调或者不调。我一般会把描述写成什么时候用这个工具、参数是什么意思、返回什么三段式。4. MySQL被低估的AI应用数据底座说到AI应用的数据存储很多人第一反应是向量数据库。但陪伴机器人的数据特征决定了MySQL才是那个不能缺的底座向量检索只是它的一个补充能力。4.1 陪伴机器人的数据分类与存储策略陪伴机器人的数据可以分成四类每类的存储策略都不一样数据类型特征存储方案索引策略用户基础数据低频写、高频读MySQL单表主键手机号唯一索引会话历史高频写、中频读MySQL按用户ID分表联合索引(user_id, created_at)长期记忆中频写、高频读MySQL向量扩展向量索引全文索引业务数据提醒、订单等低频写、低频读MySQL标准表按查询模式建索引这个分类的关键在于不要把所有数据都往向量数据库里塞。用户的基础信息、订单记录、提醒事项这些是结构化数据用MySQL的关系模型和事务能力管理是最合适的。只有那些需要语义检索的记忆文本才需要向量化。4.2 会话历史表的索引设计一个真实的慢查询案例上线第一个月我遇到过一个典型的慢查询。会话历史表有800万行数据查询某个用户最近20条消息的SQL跑了3秒多。表结构是这样的CREATE TABLE chat_history ( id BIGINT AUTO_INCREMENT PRIMARY KEY, user_id BIGINT NOT NULL, role VARCHAR(20) NOT NULL, content TEXT, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, INDEX idx_user (user_id) );问题出在idx_user这个索引上。查询语句是SELECT * FROM chat_history WHERE user_id ? ORDER BY created_at DESC LIMIT 20。MySQL用idx_user找到该用户的所有消息可能几千条然后回表取出所有字段再在内存里按created_at排序最后取前20条。这个过程中回表和排序的开销非常大。改成联合索引idx_user_time (user_id, created_at DESC)之后MySQL可以直接在索引上完成过滤和排序然后只回表取20条记录的完整数据。查询时间从3秒降到了15毫秒。ALTER TABLE chat_history ADD INDEX idx_user_time (user_id, created_at DESC);这个案例的教训是AI应用的数据访问模式往往比传统业务更集中。陪伴机器人里90%的查询都是查某个用户最近的消息这种高度集中的访问模式索引设计必须针对性地优化。4.3 用MySQL做向量检索可行但有边界MySQL 8.0之后可以通过一些扩展或者自定义函数来做向量相似度计算但说实话这不是MySQL的强项。我的做法是混合存储向量存在专门的向量索引里可以用Milvus或者PgVector也可以用MySQL存向量然后应用层算相似度但记忆的元数据、用户关联、时间戳这些存在MySQL里。具体来说长期记忆表是这样的CREATE TABLE long_term_memory ( id BIGINT AUTO_INCREMENT PRIMARY KEY, user_id BIGINT NOT NULL, memory_type VARCHAR(50) NOT NULL, content TEXT NOT NULL, embedding_id VARCHAR(100), importance_score DECIMAL(3,2) DEFAULT 0.50, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, INDEX idx_user_type (user_id, memory_type), INDEX idx_importance (user_id, importance_score DESC), FULLTEXT INDEX ft_content (content) WITH PARSER ngram );这里有几个设计决策值得说。importance_score字段是记忆的重要性评分陪伴机器人不可能记住用户说的每一句话需要有一个机制来筛选哪些记忆值得长期保留。我用的策略是用户主动提到的信息我养了只猫评分高闲聊内容评分低。检索的时候优先返回高评分的记忆。FULLTEXT INDEX用的是ngram分词器这是为了支持中文全文检索。MySQL默认的全文索引对中文支持不好必须用ngram。这个索引在混合检索里作为关键词检索的那一路和向量检索的结果做RRF融合。4.4 连接池配置别让数据库成为瓶颈陪伴机器人对MySQL的访问有两个特点读多写少和短查询为主。基于这个特点我的连接池配置是这样的spring: datasource: hikari: maximum-pool-size: 20 minimum-idle: 5 connection-timeout: 3000 idle-timeout: 600000 max-lifetime: 1800000 connection-test-query: SELECT 1maximum-pool-size设成20是因为我的应用部署在4核8G的机器上数据库是独立的8核16G实例。20个连接足够支撑每秒几百次的短查询。设太大反而会导致数据库端的连接竞争。connection-timeout设成3秒是因为陪伴机器人对延迟敏感。如果3秒还拿不到连接说明系统已经过载了这时候快速失败比让用户等着更好。注意MySQL 8.0的JDBC驱动默认useSSLtrue如果你没有配置SSL证书连接会报错。在连接串里加上useSSLfalseallowPublicKeyRetrievaltrue可以解决但生产环境建议还是配上SSL。5. 三件套的协作细节那些文档里不会写的东西单个组件讲完了真正难的是它们怎么协作。这部分是我踩坑最多的地方也是这篇文章最想分享的部分。5.1 大模型调用的超时与重试策略陪伴机器人调用大模型超时是常态。网络抖动、模型服务过载、请求排队任何一个环节都可能让一次调用超过10秒。如果不做超时控制虚拟线程会被大量占用最终拖垮整个应用。我的策略是分级超时加有限重试。普通对话请求超时设8秒重试1次RAG检索请求超时设5秒不重试因为检索失败可以降级到无检索模式工具调用请求超时设3秒不重试工具调用失败可以让模型用自然语言回复。Configuration public class LlmConfig { Bean public ChatLanguageModel chatModel() { return OpenAiChatModel.builder() .apiKey(apiKey) .modelName(gpt-4o-mini) .timeout(Duration.ofSeconds(8)) .maxRetries(1) .build(); } }重试这里有个坑重试必须保证幂等。如果第一次调用实际上成功了只是响应超时了重试会导致用户收到两条回复。我的做法是在重试前先查一下这次请求的ID有没有已经产生回复有的话直接返回已有回复。5.2 记忆写入的异步化与一致性记忆写入是一个耗时操作涉及向量化、数据库写入、索引更新。如果同步做会显著增加用户等待时间。我的做法是异步写入加最终一致性。用户消息处理的主流程只做三件事保存会话历史、调用大模型生成回复、返回回复。记忆的向量化和长期存储放到异步线程里做。这样用户感知的响应时间只包含大模型调用时间不包含记忆处理时间。Async(memoryExecutor) public void asyncUpdateMemory(Long userId, String message, String reply) { try { // 向量化 Embedding embedding embeddingModel.embed(message).content(); // 存长期记忆 memoryService.save(userId, message, embedding); // 更新用户画像 profileService.updateFromConversation(userId, message, reply); } catch (Exception e) { log.error(记忆更新失败userId{}, userId, e); // 失败不影响主流程下次对话时会重新尝试 } }这里的一致性策略是记忆可以丢但会话历史不能丢。会话历史是同步写的保证不丢记忆是异步写的允许偶尔失败。因为记忆丢了最多是机器人忘记了某件事但会话历史丢了会导致对话上下文断裂用户体验直接崩掉。5.3 事务边界哪些操作必须在一个事务里不是所有操作都需要事务。陪伴机器人里我明确划分了事务边界必须在一个事务里的会话历史写入加token配额扣减。这两个操作必须原子否则会出现消息存了但没扣配额或者扣了配额但消息没存。不需要事务的大模型调用、向量化、记忆写入。这些操作要么是外部调用无法回滚要么是允许最终一致的。需要补偿的如果大模型调用失败需要把已经扣减的配额加回去。这个用Spring的Transactional配合手动补偿实现。public String handleMessage(Long userId, String message) { // 事务1保存历史扣配额 Long historyId transactionService.saveHistoryAndDeduct(userId, message); try { // 非事务调用大模型 String reply chatModel.generate(message); // 非事务异步更新记忆 memoryService.asyncUpdateMemory(userId, message, reply); return reply; } catch (Exception e) { // 补偿回滚配额 transactionService.refundQuota(userId, historyId); throw new BusinessException(对话生成失败请重试); } }5.4 监控埋点上线后最该盯的几个指标上线后我每天必看的指标有这几个指标含义告警阈值llm_call_duration_p99大模型调用P99延迟 10秒db_connection_active数据库活跃连接数 15virtual_thread_count虚拟线程数量 5000memory_update_fail_rate记忆更新失败率 5%token_cost_per_hour每小时token消耗 预算的80%这些指标通过Micrometer暴露给PrometheusGrafana做可视化。其中memory_update_fail_rate是我自己加的因为记忆更新是异步的失败不会影响主流程但如果不监控记忆会慢慢丢失用户会觉得机器人越来越笨。6. 选型对比如果重来一次我还会这么选吗这个问题我认真想过。如果重来一次在陪伴机器人这个具体场景下我的答案还是这三个但有一些细节会调整。6.1 和Python方案的对比Python方案的优势在于AI生态丰富新的模型、新的技术出来Python总是第一个支持的。但陪伴机器人的核心挑战不在AI能力而在工程可靠性。Python的GIL、异步生态的碎片化、部署的复杂度在陪伴机器人这种长连接、高并发、状态密集的场景下劣势会被放大。如果团队全是Python背景那用Python也不是不行但要做好心理准备你需要自己解决连接池、事务、监控、部署这一整套工程问题。SpringBoot把这些都变成了默认行为这是它最大的价值。6.2 和Node.js方案的对比Node.js的异步模型天然适合IO密集场景TypeScript的类型系统也比Java更灵活。但Node.js在企业级事务管理和连接池这块成熟度确实不如Java。而且LangChain.js的成熟度比LangChain4j还要低一些很多功能还在快速迭代中用在生产环境有风险。6.3 和Go方案的对比Go的并发模型和性能都很优秀但Go生态里没有LangChain4j这样成熟的AI编排框架。你得自己封装大模型调用、自己实现记忆管理、自己处理Prompt模板。这些工作量大且容易出错。除非你的团队有很强的Go背景且愿意投入自研否则不建议。6.4 这套组合的边界在哪里这套组合不是万能的。它的边界在于如果你的陪伴机器人需要极致的推理性能或者需要跑本地大模型那Java生态的支持确实不如Python。但如果你用的是云端大模型API业务逻辑复杂需要长期迭代那这套组合是目前Java生态里最稳的选择。还有一个边界是团队规模。如果只有一两个人做原型验证Python的快速迭代优势更明显。但如果是5人以上的团队做长期产品SpringBoot的工程规范性会带来巨大的协作效率提升。7. 落地时最容易翻车的几个点最后这部分是我踩过的坑每一个都真实发生过希望能帮你省点时间。7.1 LangChain4j版本升级的兼容性LangChain4j还在快速迭代0.3x版本之间API变动不小。我遇到过升级一个小版本后ChatMemory的接口签名变了导致编译失败。建议在pom.xml里锁定版本号升级前先看Release Notes不要用LATEST。7.2 MySQL字符集中文乱码的根源陪伴机器人处理的全是中文MySQL的字符集必须是utf8mb4不是utf8。utf8在MySQL里是阉割版的只支持3字节存不了emoji和一些生僻字。建库建表的时候就要指定CREATE DATABASE companion_bot DEFAULT CHARACTER SET utf8mb4 DEFAULT COLLATE utf8mb4_unicode_ci;连接串里也要加characterEncodingutf8mb4否则JDBC驱动可能用默认字符集。7.3 虚拟线程和连接池的配合虚拟线程数量可以很多但数据库连接池是有限的。如果虚拟线程大量并发访问数据库会在连接池上排队。这时候connection-timeout的设置就很关键。设太短正常请求也会超时设太长虚拟线程会大量堆积。我的经验是设成3秒配合连接池的监控如果频繁超时说明连接池该扩容了。7.4 大模型返回内容的清洗大模型返回的内容经常带有Markdown格式、多余的换行、甚至一些特殊字符。直接存进数据库或者返回给前端会出问题。我一般会做一层清洗去掉首尾空白、把连续换行合并成一个、过滤掉不可见字符。这个清洗逻辑要放在返回给用户之前而不是存库之前因为存库保留原始内容有利于后续分析。7.5 成本控制token消耗的监控和限制陪伴机器人是token消耗大户。一个用户一天聊100轮每轮平均500 token一天就是5万token。1000个用户就是5000万token。如果不做限制账单会很吓人。我的做法是三层控制单次请求限制单次回复不超过500 token、单用户日限额每个用户每天不超过10万token、全局熔断每小时总消耗超过预算的80%时降级到更便宜的模型。这三层控制都在SpringBoot的拦截器里实现不依赖大模型服务商的控制台。Component public class TokenQuotaInterceptor implements HandlerInterceptor { Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) { Long userId getUserId(request); // 检查用户日限额 if (quotaService.isExceeded(userId)) { throw new QuotaExceededException(今日对话额度已用完); } // 检查全局熔断 if (quotaService.isGlobalCircuitBroken()) { request.setAttribute(useCheapModel, true); } return true; } }这套组合跑了大半年线上稳定支撑了日均十万级的对话量。回头看选型的核心不是选最先进的技术而是选最匹配场景的技术。陪伴机器人的场景是工程密集而非算法密集所以SpringBoot3加LangChain4j加MySQL这个组合恰好把工程能力和AI能力都覆盖到了。如果你也在做类似的方向希望这本账能帮你少走点弯路。
返回列表