第一章 从0搭建一个企业级HarnessAgent项目-机器人智能管家

📅 2026/7/15 3:36:42 👁️ 阅读次数 📝 编程学习
第一章 从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中ChatModelChatClient和 Agent 分别负责什么

我一开始也容易把它们混为一谈,后来按职责拆开看就清楚了:

组件解决的问题在项目中的位置
ChatModel如何与具体模型供应商通信AiModelConfig
ChatClient如何组织 Prompt、Advisor、Tool 和调用方式ChatService
ToolAgent 可以操作哪些外部能力RobotProductTool
Advisor在模型调用前后插入横切逻辑,类似AOP日志、安全、指标
Agent/Harness长任务、状态、工作区、记忆压缩和任务循环下一阶段引入

阶段一并没有急着把所有事情都叫作 Harness Agent。现在这层更像一个“具备工具调用能力的 Chat Agent”:先验证模型、业务工具和观测链路,再在第二阶段把它升级为带 Workspace 和可恢复 Session 的 Harness Agent。

2. Function Calling 的价值不在“会调方法”,而在边界清晰

Spring AI 通过@Tool@ToolParam把 Java 方法描述成模型可调用的工具。模型看到工具描述后,决定是否调用、调用哪个方法、参数是什么;真正的数据库或微服务访问仍然在后端代码里完成。(6.2有例子)

这样做有两个好处:

  1. 模型负责语义理解和编排,Java 负责权限、超时、异常处理与数据访问;
  2. 工具返回结构化 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 构建OpenAiApiOpenAiChatModel。这里的关键点是把连接超时和读取超时显式配置上:模型服务慢或者不可用时,不能让应用线程一直卡住。

@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,并使用modelprovider作为标签。后面接入 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.mdMEMORY.md、上下文压缩和 Token 预算管理。

我现在的理解是:Agent 的难点不在于调用一次大模型,而在于让它在多轮、长任务、失败和重启之后依然行为稳定、成本可控、过程可追踪。阶段一先把地基铺好,后面的Harness、RAG 和多 Agent 才有地方落脚。


项目代码暂时未开源,只用git私库管理,最主要的原因是目前的框架还在初步阶段,没有很大的开源意义,把这篇文章扔给AI也能写出差不多的;其次是配套的前端页面还没有时间整理,观感较差。等后续有进步了再考虑开源吧。