第一章 从0搭建一个企业级HarnessAgent项目-机器人智能管家 【阶段一】基于 Spring AI 搭起 LingNova 的第一个可用 AgentAgent开发把不稳定的模型能力治理成可控的后端能力前言这是 LingNova 企业级 Agent 项目的第一篇开发记录。最近Agent和Harness可以说是风头正盛如何把一个“黑盒”包装为企业级可控的AI应用技术路线越来越清晰。看了很多技术介绍总觉得“纸上得来终觉浅”网上也没有找到很合适的开源项目于是想自己动手搭建一个企业级HarnessAgent项目—LingNova灵核边动手边学习Agent相关技术栈这个项目的目标是做一个能做产品选型、知识问答、故障排查和资讯聚合的机器人行业AI智能管家。文章目录【阶段一】基于 Spring AI 搭起 LingNova 的第一个可用 Agent一、阶段开发计划二、技术栈介绍三、Agent VS chat四、阶段一完成了什么五、核心知识梳理1.Spring AI中 ChatModel、ChatClient 和 Agent 分别负责什么2. Function Calling 的价值不在“会调方法”而在边界清晰3. Advisor 是把“非功能需求”从业务代码里拿出去4. 多轮对话要先控制窗口再谈长期记忆六、关键代码实操1. 用 OpenAI 兼容协议配置可切换模型2. 把业务 API 包装成 Tool3. 在 ChatClient 中注册系统提示词、工具和记忆4. 用 Advisor 记录延迟和 Token5. 提供同步和流式接口七、如何本地验证八、阶段一踩到的坑和我的理解1. 工具异常不能直接把整个对话打断2. “支持多轮”不等于无限保存聊天记录3. 先把可观测性放进第一天的代码里九、下一步从 Chat Agent 走向 Harness Agent一、阶段开发计划阶段一基础框架搭建目标接入 Spring AI Alibaba 与 Deepseek实现第一个能调用工具的 Agent。阶段二Harness 工程化改造目标引入 AgentScope Java Harness实现有状态、可恢复、有记忆的 Agent解决上下文爆炸问题。阶段三多 Agent RAG 工具生态目标实现复杂任务自动拆解引入 RAG、多 Agent 协作、MCP 工具协议。阶段四企业级治理目标让 Agent 具备生产级能力评估体系、权限隔离、安全护栏、可观测性。二、技术栈介绍由于我本身是做Java后端开发所以这个项目注重探索如何在传统Java后端项目中构建Agent项目因此使用Spring AI相关的技术框架开始搭建整体的技术栈如下层级技术后端框架Spring Boot 3.2 Spring AI Alibaba 1.1.2.xAgent 运行时AgentScope Java Harness 2.x大模型DashScope通义千问、DeepSeek、OpenAI 兼容接口向量数据库PGVector / Milvus缓存/会话Redis可观测性OpenTelemetry Langfuse / LangSmith部署Docker Kubernetes三、Agent VS chat可能是网页版AI对话用的太多形成惯性思维刚开始接触大模型时很容易把 Agent 理解成前端传一句话后端把这句话交给模型再把结果返回要加功能就优化提示词。虽然与Agent的交流方式基本都是通过聊天对话但是与传统对话方式不同的是Agent背后有更多的业务调用逻辑。以机器人选型为例用户问“预算 20 万想做焊接产线推荐什么机器人”模型不应该凭训练数据编一个型号和价格它应该先判断需要查平台自己的产品数据再根据工具返回的结果组织答案。这里的“判断—调用—整合”才是 Agent 最小但关键的闭环。所以阶段一我给自己定的目标不是堆功能而是先打通下面这条链路用户请求 - ChatController - ChatService / ChatClient - Advisor 链日志、敏感词、指标 - 大模型判断是否调用 Tool - RobotProductTool 查询机器人服务 - 大模型整合业务数据 - 返回同步响应或 SSE 流这套结构后面可以继续承接 Harness 的 Session、Workspace、长期记忆和多 Agent而不会推翻已经写好的业务入口。四、阶段一完成了什么阶段一实际完成的能力包括基于 Spring Boot 3.4、Java 17 和 Spring AI 1.0 搭建 AI 服务通过 OpenAI 兼容协议统一接入 DeepSeek、GLM 等模型避免业务层绑定单一厂商提供同步对话、SSE 流式对话两个接口将机器人产品查询封装成 Spring AI Tool让模型按需调用真实业务 API使用 Redis 保存最近 20 条消息实现多轮上下文和服务重启后的恢复通过 Advisor 完成调用日志、敏感词拦截、Token 和延迟指标采集用 Actuator 暴露健康检查和指标为后续 Prometheus/Grafana 做准备。在第一阶段只是先做到AI能够调用deepseek进行对话、大模型可以正常判断并调用tool、调用日志记录、敏感词拦截、指标采集、简单的记忆功能把架子搭起来后面再慢慢进化。五、核心知识梳理在这里我只梳理代码实践过程中关键一些的知识概念不会特别详细复述完整概念更详细全面的知识可以看B站的一些系统课因为这是个实践型项目。1.Spring AI中ChatModel、ChatClient和 Agent 分别负责什么我一开始也容易把它们混为一谈后来按职责拆开看就清楚了组件解决的问题在项目中的位置ChatModel如何与具体模型供应商通信AiModelConfigChatClient如何组织 Prompt、Advisor、Tool 和调用方式ChatServiceToolAgent 可以操作哪些外部能力RobotProductTool等Advisor在模型调用前后插入横切逻辑类似AOP日志、安全、指标Agent/Harness长任务、状态、工作区、记忆压缩和任务循环下一阶段引入阶段一并没有急着把所有事情都叫作 Harness Agent。现在这层更像一个“具备工具调用能力的 Chat Agent”先验证模型、业务工具和观测链路再在第二阶段把它升级为带 Workspace 和可恢复 Session 的 Harness Agent。2. Function Calling 的价值不在“会调方法”而在边界清晰Spring AI 通过Tool和ToolParam把 Java 方法描述成模型可调用的工具。模型看到工具描述后决定是否调用、调用哪个方法、参数是什么真正的数据库或微服务访问仍然在后端代码里完成。6.2有例子这样做有两个好处模型负责语义理解和编排Java 负责权限、超时、异常处理与数据访问工具返回结构化 DTO而不是一大段拼接字符串模型更容易引用后续也更容易审计。需要特别注意Tool 的描述和参数描述就是给模型看的“接口文档”。描述写得含糊模型就更容易选错工具或传错参数。它不是普通注释应该按 API 契约认真维护。3. Advisor 是把“非功能需求”从业务代码里拿出去真实项目里对话服务还会不断长出日志、安全、限流、埋点、审计等需求。如果都塞进ChatService很快就会变成难以维护的流程代码。Spring AI 的 Advisor 可以在请求前和响应后执行逻辑适合处理这些横切关注点。当前项目的默认顺序是ChatLoggingAdvisor - SensitiveWordAdvisor - AgentMetricsAdvisor - 模型调用其中敏感词拦截在调用模型前短路请求指标 Advisor 在响应后读取Usage把 Token 和耗时交给 Micrometer。这个顺序不是绝对标准但原则很简单安全校验要尽早埋点要能拿到最终结果日志要避免把密钥或完整隐私数据写出去。4. 多轮对话要先控制窗口再谈长期记忆大模型并不会天然“记住”用户。所谓多轮对话本质是下一次调用时把历史消息一起带上。历史无限累积会导致 Token 成本和延迟线性上涨所以阶段一先使用窗口记忆只保留最近 20 条消息并放到 Redis。短期记忆最近 N 条消息保证当前话题连续 长期记忆用户偏好、事实摘要下一阶段再用 Harness MEMORY.md 管理这是一个有意的分层。不要一开始就把全部聊天记录塞进 Prompt先让窗口记忆稳定运行后面再引入压缩和长期记忆问题会清晰很多。六、关键代码实操1. 用 OpenAI 兼容协议配置可切换模型模型配置统一放在application.yml中。真实的 API Key 不应该提交到 Git这里只保留环境变量写法。lingnova:ai:models:chat:provider:${AI_CHAT_PROVIDER:deepseek}providers:deepseek:base-url:https://api.deepseek.comapi-key:${DEEPSEEK_API_KEY}model:${AI_CHAT_MODEL:deepseek-chat}glm:base-url:https://open.bigmodel.cn/api/paas/v4api-key:${GLM_API_KEY}model:glm-4在配置类中根据当前 provider 构建OpenAiApi和OpenAiChatModel。这里的关键点是把连接超时和读取超时显式配置上模型服务慢或者不可用时不能让应用线程一直卡住。BeanpublicChatModelchatModel(MultiModelPropertiesprops,AiContentPropertiesaiProps){varconfigprops.getChat().resolveProvider();RestClient.BuilderrestClientbuildRestClientBuilder(aiProps.getChatConnectTimeoutSeconds(),aiProps.getChatReadTimeoutSeconds());OpenAiApiapiOpenAiApi.builder().baseUrl(config.getBaseUrl()).apiKey(config.getApiKey()).restClientBuilder(restClient).build();returnOpenAiChatModel.builder().openAiApi(api).defaultOptions(OpenAiChatOptions.builder().model(config.getModel()).build()).build();}2. 把业务 API 包装成 Tool产品查询不是让模型直接连数据库而是由RobotProductTool调用lingnova-robot-service的 REST API。下面是按关键字查询产品的精简实现Tool(description按关键字搜索机器人产品。当用户提到具体产品、品牌、型号或应用场景时调用返回产品简要信息。)publicListRobotProductSummarysearchRobots(ToolParam(description搜索关键字例如焊接机器人、人形机器人、优必选)Stringkeyword,ToolParam(description最大返回数量默认 5最大 20)Integerlimit){intsizelimit!null?Math.min(limit,20):5;try{ResultListRobotProductSummaryresultrestClient.get().uri(uriBuilder-uriBuilder.path(/api/robots/search).queryParam(keyword,keyword).build()).retrieve().body(newParameterizedTypeReference(){});returnunwrap(result,size);}catch(Exceptione){log.warn(搜索机器人产品失败: keyword{}, error{},keyword,e.getMessage());returnList.of();}}这里刻意做了三件小事限制limit上限、返回结构化对象、异常时返回空列表并记录日志。它们看起来普通但直接决定了模型上下文是否可控、调用失败时是否会拖垮一次对话。3. 在ChatClient中注册系统提示词、工具和记忆ChatService启动时初始化ChatClient。系统提示词负责定义边界defaultTools注册业务工具记忆 Advisor 在每次调用时按会话 ID 注入上下文。PostConstructprivatevoidinit(){chatMemoryAdvisorMessageChatMemoryAdvisor.builder(chatMemory).build();chatClientchatClientBuilder.defaultSystem(SYSTEM_PROMPT).defaultTools(robotProductTool,robotQueryTool,knowledgeQueryTool).build();}publicStringchat(Stringmessage,StringsessionId){returnchatClient.prompt().user(message).advisors(chatMemoryAdvisor).advisors(a-a.param(ChatMemory.CONVERSATION_ID,sessionId)).call().content();}sessionId是多轮对话的关键。前端首次不传时由服务端生成后续每次带回即可命中同一个 Redis 会话如果没有它表面上虽然是聊天实际上每一轮都是独立请求。对应的 Redis 窗口记忆配置如下BeanpublicChatMemorychatMemory(StringRedisTemplateredisTemplate){returnMessageWindowChatMemory.builder().chatMemoryRepository(newRedisChatMemoryRepository(redisTemplate)).maxMessages(20).build();}4. 用 Advisor 记录延迟和 Token下面的代码展示了指标 Advisor 最核心的部分请求前记录时间响应后读取模型返回的 Token 用量并写入 Micrometer。OverridepublicChatClientRequestbefore(ChatClientRequestrequest,AdvisorChainchain){request.context().put(agent_metrics_start_time,System.nanoTime());returnrequest;}OverridepublicChatClientResponseafter(ChatClientResponseresponse,AdvisorChainchain){longdurationMsTimeUnit.NANOSECONDS.toMillis(System.nanoTime()-(Long)response.context().get(agent_metrics_start_time));Usageusageresponse.chatResponse().getMetadata().getUsage();meterRegistry.counter(lingnova.ai.chat.requests,tags).increment();meterRegistry.timer(lingnova.ai.chat.latency,tags).record(durationMs,TimeUnit.MILLISECONDS);meterRegistry.summary(lingnova.ai.chat.tokens.total,tags).record(usage.getTotalTokens());returnresponse;}当前已经采集了请求次数、端到端耗时、输入 Token、输出 Token、总 Token并使用model、provider作为标签。后面接入 Prometheus 后就可以回答一些以前很难回答的问题哪个模型慢、哪类请求贵、工具调用是否真的改善了回答质量。5. 提供同步和流式接口同步接口适合后台调用或快速验证面向用户的聊天页面更适合 SSE 流式输出。PostMapping(/chat)publicResponseEntityMapString,Objectchat(RequestParamStringmessage,RequestParam(requiredfalse)StringsessionId){Stringid(sessionIdnull||sessionId.isBlank())?UUID.randomUUID().toString():sessionId;returnResponseEntity.ok(Map.of(sessionId,id,reply,chatService.chat(message,id)));}GetMapping(value/stream,producesMediaType.TEXT_EVENT_STREAM_VALUE)publicFluxStringchatStream(RequestParamStringmessage,RequestParam(requiredfalse)StringsessionId){Stringid(sessionIdnull||sessionId.isBlank())?UUID.randomUUID().toString():sessionId;returnchatService.chatStream(message,id);}七、如何本地验证同步接口验证基础对话curl-XPOSThttp://localhost:9005/api/ai/chat/chat?message推荐一款适合焊接产线的机器人响应会返回服务端生成的sessionId。把它用于下一轮请求验证上下文是否连贯curl-XPOSThttp://localhost:9005/api/ai/chat/chat?sessionId上一步返回的sessionIdmessage预算控制在20万以内流式接口可以直接用浏览器或前端EventSource访问GET http://localhost:9005/api/ai/chat/stream?message介绍一下协作机器人sessionIdyour-session-id最后检查基础可观测性GET http://localhost:9005/actuator/health GET http://localhost:9005/actuator/metrics/lingnova.ai.chat.latency GET http://localhost:9005/actuator/metrics/lingnova.ai.chat.tokens.total八、阶段一踩到的坑和我的理解1. 工具异常不能直接把整个对话打断下游机器人服务偶发超时是正常情况。阶段一先在 Tool 内捕获异常返回空结果并记录告警日志下一阶段会继续补上统一的超时、重试、熔断和幂等能力。比起“工具报错就 500”让模型知道“没有查到数据”通常更接近用户需要的体验。2. “支持多轮”不等于无限保存聊天记录Redis 解决了会话持久化不解决上下文膨胀。20 条窗口是当前阶段的成本控制策略不是最终答案。后续 Harness 的 Compaction 和长期记忆才是让长对话可持续的关键。3. 先把可观测性放进第一天的代码里传统接口慢了还可以看 SQL、线程池Agent 慢了则可能是模型、Prompt、上下文、工具服务中的任意一环。没有延迟和 Token 指标后面只能凭感觉优化。这个项目从第一阶段就埋点是为了后面每个优化都有数据依据。九、下一步从 Chat Agent 走向 Harness Agent阶段一验证的是“模型能否进入真实业务系统”。下一阶段才是我最想深入的 Harness为每个用户和会话建立可恢复的 Workspace引入AGENTS.md、MEMORY.md、上下文压缩和 Token 预算管理。我现在的理解是Agent 的难点不在于调用一次大模型而在于让它在多轮、长任务、失败和重启之后依然行为稳定、成本可控、过程可追踪。阶段一先把地基铺好后面的Harness、RAG 和多 Agent 才有地方落脚。项目代码暂时未开源只用git私库管理最主要的原因是目前的框架还在初步阶段没有很大的开源意义把这篇文章扔给AI也能写出差不多的其次是配套的前端页面还没有时间整理观感较差。等后续有进步了再考虑开源吧。