企业级 AI 职业规划智能体(Career Planning Agent)— 项目复盘笔记


1. 本次项目核心功能与技术栈(含实现要点)

1.1 产品侧核心能力

能力 说明
职业规划师对话 固定「资深职业规划师」系统提示,强调可落地、拒绝空话;支持多轮上下文。
SSE 流式输出 职业规划师与 Manus 智能体均通过服务端推送(SSE)把生成过程流式交给前端,改善首字延迟与体验。
持久化会话记忆 chatId 为键,将 USER / ASSISTANT 行追加写入 chat-memory/<chatId>.log,新请求时拼接最近若干行进 Prompt。
RAG 检索增强 文档经嵌入模型写入向量库;对话前可做查询重写;检索结果通过 Spring AI 的 QuestionAnswerAdvisor 注入到对话中。
工具调用 + 类 ReAct 智能体(Manus) 基于 ToolCallingManagerToolCallback,集成 PDF 生成、文件操作、网页搜索/爬取、资源下载、终端操作、终止工具等;多步循环直至结束或达最大步数。
生成物下载 工具生成文件注册到内存 GeneratedFileStore,通过 fileId + 会话 chatId 校验后提供下载。
前端双模式 首页切换「职业规划师」与「智能体」两个聊天室;统一霓虹/粒子视觉;分别消费 SSE(职业规划师用 EventSource + GET URL;Manus 用自定义 artifact 事件等)。

1.2 技术栈与关键依赖

后端

  • Java 21 + Spring Boot 3.4.4:Web、校验、异步 SSE(SseEmitter)、Reactive 的 Flux(职业规划师流式接口)。
  • Spring AI Alibaba DashScopespring-ai-alibaba-starter-dashscope):统一对接通义等模型;ChatClient 流式/同步调用;EmbeddingModel 用于向量。
  • Spring AI 向量存储:默认 SimpleVectorStore(内存);可选 PgVectorspring-ai-starter-vector-store-pgvector + JDBC + PostgreSQL 驱动);application.yml 中通过 app.rag.use-pgvector 等开关切换实际用于 RAG 的 VectorStore
  • RAG 周边spring-ai-markdown-document-reader 读 Markdown 知识库;自定义 QueryRewriter(内部用 RewriteQueryTransformer)把用户问题改写成更利于检索的表述;可选云顾问 ObjectProvider<Advisor>QuestionAnswerAdvisor 组合。
  • 其他:Knife4j / springdoc 文档;Jsoup 网页解析;iText 生成 PDF;Hutool;Kryo(FileBasedChatMemory 实现);MCP Client 依赖在配置中默认 enabled: false

前端(ai-agent-fronted/

  • Vue 3 + TypeScript + Vite + Vue Router;Axios 配 baseURL 指向后端 /api
  • SSE:职业规划师侧用 EventSource + GET 查询参数createSseGetUrl);Manus 侧同样 EventSource,并解析自定义事件 artifact(JSON:fileIdfilename)以触发下载。
  • 体验createTypewriter 做打字机队列;CareerChatView 中对 AI 气泡使用 reactive 包裹消息对象,避免流式 push 时 Vue 对深层属性追踪不到导致「空白气泡」类问题。

1.3 值得单独展开的技术细节

(1)多轮记忆:PersistentMemoryService

  • 实现是纯文本日志buildMessageWithHistory 把最近 N 行读入,拼成「历史对话 + 用户最新问题」一段 user 文本,而不是用 Spring AI 官方的 ChatMemory Advisor 贯穿 ChatClient
  • 流式持久化doChatByStream 使用 doFinally 在流结束、取消或出错时统一 append,避免仅用 doOnComplete 时在客户端提前断开导致本轮 assistant 内容未落盘、下一轮丢失上下文(代码里已有明确注释说明根因)。

(2)RAG 管线

  • 异步启动加载ApplicationRunner 在应用启动后再 loadMarkDowns() → 关键词增强 MyKeywordEnrichervectorStore.add,避免启动阶段网络/嵌入超时拖垮启动;无有效 API Key 时以空库启动并打日志。
  • 查询重写RewriteQueryTransformer 用同一 ChatModel 对「带历史的整段文本」做 transform,再交给向量检索,缓解指代不明、省略主语等造成的检索失败。

(3)Manus / ToolCallAgent

  • DashScope 选项里 withInternalToolExecutionEnabled(false):由 Spring AI 的 ToolCallingManager 显式执行 tool call,便于记录工具结果再二次调用模型生成最终回答。
  • thinkact 循环think 里发起带 toolCallbackscall;若有 tool call 则 actexecuteToolCalls 并再 call 一次产出最终中文回答;terminate 工具会结束状态。

(4)接口形态说明(与文档对齐)

  • 当前 AiController 中职业规划师 SSE 为 GET /ai/career_app/chat/sse?message=&chatId= 返回 Flux<ServerSentEvent<String>>README 中仍写 POST + JSON,属于文档与实现不一致,接入或联调时若以 README 为准会踩坑(应以前端与 AiController 为准)。

2. 技术难点与「当时可能不熟」的知识点

  1. Spring AI 版本矩阵pom.xml 中同时存在 M6 / M7 等不同里程碑版本的 Spring AI 组件,与 Alibaba Starter 组合时易出现类路径、API 变更、Bean 定义冲突(Git 中有「Bean 注入冲突报错解决」类提交)。需要理解 Spring Boot 自动配置、@Qualifier@ConditionalOnPropertyObjectProvider 的用法。
  2. Reactive Flux SSE 与 Servlet SseEmitter 混用:职业规划师用 WebFlux 风格的 Flux<ServerSentEvent>,Manus 用 MVC SseEmitter + CompletableFuture.runAsync;要分别理解背压、线程模型、客户端断开(如 GlobalExceptionHandlerClientAbortException 的处理)。
  3. DashScope 工具调用:关闭内部工具执行、自行 ToolCallingManager 执行,需理解 AssistantMessage 中的 toolCallsToolResponseMessage 与多轮 message list 的维护顺序。
  4. 向量检索质量:仅 embedding + topK 往往不够;本项目通过 查询重写关键词 enrich metadata、(可选)过滤表达式 组合提升召回与精度,涉及 RAG 各层(预检索、检索、后处理)的分工。
  5. 前端流式 + Vue 响应式:增量更新 ref 数组内普通对象的字段可能不触发视图更新;需熟悉 reactive 包裹或不可变替换等范式。
  6. EventSource 限制:仅 GET、无法自定义部分请求头;与 POST SSE(fetch + ReadableStream)的取舍、以及连接关闭时 onerror 与正常结束的区分(代码里对 CONNECTING 状态做了区分)。

3. 出现的 Bug 及根因(基于代码与演进记录)

现象 / 主题 可能根因 依据
流式对话「记不住」上一轮 assistant 若只在 doOnComplete 写日志,用户中途关页面或取消订阅,完成回调不触发,记忆未追加。 CareerPlanningAgentApp#doChatByStream 使用 doFinally 修复。
前端 AI 气泡流式时空白、结束才一次性显示 对数组元素用普通对象,content 增量未建立正确响应式追踪。 CareerChatView.vue 注释与 reactive 用法。
SSE 客户端断开刷错误日志 / 异常 断开连接时服务端仍写响应,抛出 ClientAbortException / AsyncRequestNotUsableException GlobalExceptionHandler 单独捕获并 debug 记录。
README 与真实接口不一致 接口从 POST JSON 改为 GET 查询参数后,文档未完全同步。 README vs AiController
启动卡住或向量加载失败 启动同步调用嵌入 API 或读大文档易导致超时;网络不稳定时失败。 改为 ApplicationRunner 异步加载 + try/catch 降级。
多个 VectorStore Bean 冲突 内存库、PgVector、自动配置同时注册,注入点未限定 @Qualifier Git 提交信息「Bean注入冲突报错解决」+ 当前 resolveRagVectorStore 与双 Bean 设计。

4. 代码与架构层面的不足

  1. AIManus 默认 Spring 单例 + 有状态 BaseAgentmessageListstate 等在实例字段上;并发两个 Manus 请求会共享同一 agent 状态,存在竞态与串话风险。更合理的是每请求/每会话一个 Agent 实例(prototype Bean、AgentInstance 工厂或把状态抽到会话级对象)。
  2. 记忆机制双轨:存在 ChatMemory Bean(FileBasedChatMemory + Kryo)与 PersistentMemoryService(纯文本 log)两套实现,主流程职业规划师只用后者,易造成维护困惑与目录混用(若 Kryo 与 log 同时写同一目录需格外小心;当前需再确认是否仅一处生效)。
  3. ReActAgent#step 异常处理printStackTrace() 与吞掉细节不利于生产排障;应统一日志与错误码。
  4. 依赖版本不统一:Spring AI 相关 artifact M6/M7 混用,长期增加安全补丁与升级成本,建议对齐到同一 BOM 或版本线。
  5. Manus 步数AIManussetMaxSteps(3) 较激进,复杂任务易未执行完即截断,与「全能助手」人设可能冲突,宜配置化或按任务类型区分。
  6. 下载与元数据GeneratedFileStore 仅内存 map,进程重启即丢失 fileId;无 TTL 清理,长期运行可能泄漏路径引用(若配合磁盘文件生命周期需再设计)。
  7. 文档/接口契约:缺少 OpenAPI 与真实 SSE 参数源的单一事实来源(README 过时)。

5. 时间与流程问题(结合 Git 阶段划分)

从提交信息可见大致节奏:后端能力(RAG、工具、记忆)→ PgVector / 跨域 / Bean 修复 → 企业级强化 → 前端两页 + UI → 精简接口与下载能力 → README 更新

可反思的流程点:

  • 文档与实现同步:接口方法(GET/POST)变更后应同 PR 更新 README 与前端 config,避免后人按文档联调失败。
  • 阶段二「企业级」与后续删接口:若曾对外暴露多接口再删减,说明早期接口边界未先冻结,可增加「最小可用 API 列表」评审。
  • 密钥与本地配置:DashScope、Search API 等依赖环境变量;需在 README 或 application-local.yml.example 中写清无 Key 时的降级行为(如空向量库、搜索失败),减少新人排查时间。

6. 可改进与优化方向

架构与可靠性

  • Manus:按请求创建 Agent 或显式会话锁;SSE 异步线程池使用命名线程池(便于监控)而非默认 ForkJoinPool
  • 记忆:统一为 一种实现(要么 Spring AI ChatMemory Advisor,要么自研 log),并定义最大上下文 token/字符截断策略,避免超长 log 撑爆模型窗口。
  • GeneratedFileStore:增加 TTL + 定期清理;可选落库或 Redis,支持多实例部署。

效果与成本

  • RAG:对重写查询做 A/B 或离线评测(命中率、回答引用率);调节 topK、阈值与 chunk 策略。
  • 模型调用:对简单轮次可跳过重写以省一次 LLM;缓存常见问题的检索结果。

工程化

  • 统一 Spring AI 版本;引入 spring-ai-bom(若官方提供与 Boot 3.4 匹配的组合)。
  • 契约测试:SSE 集成测试(TestRestTemplate / WebTestClient)覆盖 done 事件与 [DONE]
  • 国际化与文案:后端错误信息中英混用(如 chatId 校验),可统一为产品语言策略。

安全

  • app.security.enabled 关闭时便于开发;生产应开启 API Key 拦截器(项目中已有 ApiSecurityInterceptor 雏形)并限制 Manus 工具(尤其终端操作)的白名单与沙箱

小结

本项目完整串联了 大模型对话、文件记忆、向量 RAG、查询重写、工具调用、SSE 前后端协同与生成物下载,技术密度适合作为「AI 应用工程化」学习样本。最值得带走的三条经验是:(1)流式场景下持久化时机要用 doFinally 等覆盖取消/错误;(2)有状态 Agent 勿用单例共享;(3)文档与接口必须同源,否则复盘成本会转嫁给下一个自己或队友。

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐