第一章 从0搭建一个企业级HarnessAgent项目-机器人智能管家
【阶段一】基于 Spring AI 搭起 LingNova 的第一个可用 Agent
Agent开发:
把不稳定的模型能力治理成可控的后端能力
前言:这是 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 包装成 Tool
- 3. 在 `ChatClient` 中注册系统提示词、工具和记忆
- 4. 用 Advisor 记录延迟和 Token
- 5. 提供同步和流式接口
- 七、如何本地验证
- 八、阶段一踩到的坑和我的理解
- 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.x |
| Agent 运行时 | 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 | 如何与具体模型供应商通信 | AiModelConfig |
ChatClient | 如何组织 Prompt、Advisor、Tool 和调用方式 | ChatService |
| Tool | Agent 可以操作哪些外部能力 | 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){varconfig=props.getChat().resolveProvider();RestClient.BuilderrestClient=buildRestClientBuilder(aiProps.getChatConnectTimeoutSeconds(),aiProps.getChatReadTimeoutSeconds());OpenAiApiapi=OpenAiApi.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="按关键字搜索机器人产品。当用户提到具体产品、品牌、型号或应用场景时调用,返回产品简要信息。")publicList<RobotProductSummary>searchRobots(@ToolParam(description="搜索关键字,例如:焊接机器人、人形机器人、优必选")Stringkeyword,@ToolParam(description="最大返回数量,默认 5,最大 20")Integerlimit){intsize=limit!=null?Math.min(limit,20):5;try{Result<List<RobotProductSummary>>result=restClient.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(){chatMemoryAdvisor=MessageChatMemoryAdvisor.builder(chatMemory).build();chatClient=chatClientBuilder.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){longdurationMs=TimeUnit.NANOSECONDS.toMillis(System.nanoTime()-(Long)response.context().get("agent_metrics_start_time"));Usageusage=response.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")publicResponseEntity<Map<String,Object>>chat(@RequestParamStringmessage,@RequestParam(required=false)StringsessionId){Stringid=(sessionId==null||sessionId.isBlank())?UUID.randomUUID().toString():sessionId;returnResponseEntity.ok(Map.of("sessionId",id,"reply",chatService.chat(message,id)));}@GetMapping(value="/stream",produces=MediaType.TEXT_EVENT_STREAM_VALUE)publicFlux<String>chatStream(@RequestParamStringmessage,@RequestParam(required=false)StringsessionId){Stringid=(sessionId==null||sessionId.isBlank())?UUID.randomUUID().toString():sessionId;returnchatService.chatStream(message,id);}七、如何本地验证
同步接口验证基础对话:
curl-XPOST"http://localhost:9005/api/ai/chat/chat?message=推荐一款适合焊接产线的机器人"响应会返回服务端生成的sessionId。把它用于下一轮请求,验证上下文是否连贯:
curl-XPOST"http://localhost:9005/api/ai/chat/chat?sessionId=上一步返回的sessionId&message=预算控制在20万以内"流式接口可以直接用浏览器或前端EventSource访问:
GET http://localhost:9005/api/ai/chat/stream?message=介绍一下协作机器人&sessionId=your-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也能写出差不多的;其次是配套的前端页面还没有时间整理,观感较差。等后续有进步了再考虑开源吧。