Harness Engineering 实战:用 Spring AI Alibaba 构建可控的 AI 智能体
Harness Engineering 实战:用 Spring AI Alibaba 构建可控的 AI 智能体
从理论到 Windows 本地运行,完整示例带你掌握 AI 工程化新范式
Spring AI Alibaba Agent 结构化输出(Structured Output)完整指南:
https://blog.csdn.net/BADAO_LIUMANG_QIZHI/article/details/162345661
基于上述示例。
一、什么是 Harness Engineering?
Harness Engineering(驾驭工程)是 2026 年 AI 工程化领域兴起的一种新范式。它的核心思想是:将重心从优化 AI 模型本身,转移到为 AI 智能体(Agent)构建一个可靠、可控、可维护的运行环境。
如果把大语言模型比作一匹力量强大但难以预测的野马,那么 Harness(马具) 就是套在它身上用来引导和控制的整套装备。业界有一个共识性的公式:
Agent = Model + Harness
这意味着,一个真正能用的 AI 智能体,不仅需要一个聪明的大脑(模型),更离不开一套完备的“马具”系统——它负责处理模型推理之外的所有“杂务”,如工具调用、记忆管理、错误处理、安全护栏等。
Harness Engineering 正是系统化地设计、构建和维护这套“马具”的方法论。它包含几个关键实践:
| 组件 | 类型 | 作用 |
|---|---|---|
| Rules(规则) | 软约束 | 声明式的 Markdown 规范,告诉 Agent“什么不能做” |
| Skills(技能) | 半硬约束 | 步骤化的操作手册,告诉 Agent“具体怎么做” |
| Gate(门禁) | 硬约束 | 可执行的检查脚本,对 Agent 输出进行强制校验,“不通过就拦截” |
| State(状态) | 上下文管理 | 记录 Agent 的进度和上下文,实现跨会话记忆 |
| Instructions(指令) | 行为指导 | 告诉 Agent 做什么、按什么顺序做 |
| Verification(验证) | 质量保证 | 只有通过测试才算任务完成 |
本文将基于 Spring AI Alibaba 框架的结构化输出能力,结合 Harness Engineering 理念,在 Windows 环境下从零构建一个带“马具”的 AI 智能体。
注:
博客:
https://blog.csdn.net/badao_liumang_qizhi
二、Windows 本地环境准备
2.1 安装必要软件
| 软件 | 版本要求 | 下载地址 |
|---|---|---|
| JDK | 17 或更高 | Oracle JDK 或 OpenJDK |
| Maven | 3.8+ | https://maven.apache.org/download.cgi |
| IntelliJ IDEA | 2023.3+(或 VS Code) | https://www.jetbrains.com/idea/ |
| curl | 任意版本(用于测试) | Windows 10/11 内置,或从 curl.se 下载 |
| DashScope API Key | 阿里云百炼平台获取 | https://bailian.console.aliyun.com/ |
提示:安装完成后,在命令行执行
java -version、mvn -version验证环境变量是否配置正确。
三、项目依赖与配置文件
3.1 Maven 项目配置(pom.xml)
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.2.5</version>
</parent>
<groupId>com.example.ai</groupId>
<artifactId>spring-ai-harness-demo</artifactId>
<version>1.0.0</version>
<properties>
<java.version>17</java.version>
<spring-ai-alibaba.version>1.1.2.0</spring-ai-alibaba.version>
<jackson.version>2.17.2</jackson.version>
</properties>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>com.fasterxml.jackson</groupId>
<artifactId>jackson-bom</artifactId>
<version>${jackson.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.cloud.ai</groupId>
<artifactId>spring-ai-alibaba-agent-framework</artifactId>
<version>${spring-ai-alibaba.version}</version>
</dependency>
<dependency>
<groupId>com.alibaba.cloud.ai</groupId>
<artifactId>spring-ai-alibaba-starter-dashscope</artifactId>
<version>${spring-ai-alibaba.version}</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-core</artifactId>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-annotations</artifactId>
</dependency>
</dependencies>
<repositories>
<repository>
<id>spring-milestones</id>
<name>Spring Milestones</name>
<url>https://repo.spring.io/milestone</url>
</repository>
</repositories>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
注意:必须显式管理 Jackson 版本,避免因版本冲突导致
NoSuchMethodError。
3.2 Spring Boot 配置文件(application.yml)
在 src/main/resources/application.yml 中:
server:
port: 885
spring:
ai:
dashscope:
api-key: ${DASHSCOPE_API_KEY} # 从环境变量读取,或直接写明文(不推荐)
chat:
options:
model: qwen-max
logging:
level:
com.alibaba.cloud.ai: debug
com.example.ai: debug
在 Windows 命令行中设置环境变量(临时):
set DASHSCOPE_API_KEY=你的API密钥
或者永久设置(通过系统环境变量面板)。
四、完整代码实现
4.1 项目包结构
src/main/java/com/example/ai/
├── SpringAiHarnessDemoApplication.java # 启动类
├── config/
│ └── HarnessAgentConfig.java # Agent配置
├── controller/
│ └── HarnessController.java # REST API
├── service/
│ └── HarnessAgentService.java # 业务服务
├── model/
│ ├── ContactInfo.java # 联系人POJO
│ └── ProductReview.java # 评价POJO
└── harness/
├── rules/
│ └── ExtractionRules.md # 规则文件(软约束)
├── skills/
│ └── ReviewAnalysisSkill.java # 评价分析技能(半硬约束)
└── gates/
└── OutputValidator.java # 输出门禁(硬约束)
4.2 数据模型(POJO)
ContactInfo.java(简单 POJO)
package com.example.ai.model;
public class ContactInfo {
private String name;
private String email;
private String phone;
public ContactInfo() {}
public ContactInfo(String name, String email, String phone) {
this.name = name;
this.email = email;
this.phone = phone;
}
// 省略 getter/setter(请自行生成,或使用 Lombok)
// 关键方法:getName()、setName()、getEmail()、setEmail()、getPhone()、setPhone()
}
ProductReview.java(嵌套 POJO)
package com.example.ai.model;
import java.util.Arrays;
public class ProductReview {
private int rating;
private String sentiment; // "positive", "neutral", "negative"
private String[] keyPoints;
private ReviewDetails details;
// 内部类
public static class ReviewDetails {
private String[] pros;
private String[] cons;
private String summary;
// getter/setter 省略
}
// getter/setter 省略
}
完整代码请参见前文,此处仅示意。
4.3 Harness 组件
① 规则层(软约束):src/main/resources/harness/rules/ExtractionRules.md
# 联系人提取规则
## 必填字段
- name: 必须提取完整姓名(中文或英文)
- email: 必须提取完整邮箱地址
- phone: 必须提取完整电话号码(含区号)
## 格式要求
- 所有字段值必须去除首尾空格
- 电话号统一为字符串格式,保留原始格式
## 禁止行为
- 不要编造任何字段值
- 如果某个字段在原文中找不到,设置为空字符串 ""
② 技能层(半硬约束):ReviewAnalysisSkill.java
package com.example.ai.harness.skills;
import com.example.ai.model.ProductReview;
import org.springframework.stereotype.Component;
@Component
public class ReviewAnalysisSkill {
public String buildPrompt(String reviewText) {
return """
请分析以下商品评价,按标准格式输出 JSON:
分析步骤:
1. 提取评分(1-5星整数)
2. 判断情感倾向:positive / neutral / negative
3. 提取关键点(至少2个,最多5个)
4. 分别列出优点和缺点
5. 生成一句话总结
评价文本:
""" + reviewText;
}
public ProductReview postProcess(ProductReview review) {
if (review.getRating() < 1) review.setRating(1);
if (review.getRating() > 5) review.setRating(5);
if (review.getKeyPoints() == null || review.getKeyPoints().length == 0) {
review.setKeyPoints(new String[]{"无关键点"});
}
return review;
}
}
③ 门禁层(硬约束):OutputValidator.java
package com.example.ai.harness.gates;
import com.example.ai.model.ContactInfo;
import com.example.ai.model.ProductReview;
import org.springframework.stereotype.Component;
import java.util.ArrayList;
import java.util.List;
@Component
public class OutputValidator {
public List<String> validateContact(ContactInfo contact) {
List<String> errors = new ArrayList<>();
if (contact == null) { errors.add("联系人信息为空"); return errors; }
if (contact.getName() == null || contact.getName().trim().isEmpty())
errors.add("姓名不能为空");
if (contact.getEmail() == null || !contact.getEmail().contains("@"))
errors.add("邮箱格式无效");
if (contact.getPhone() == null || contact.getPhone().trim().isEmpty())
errors.add("电话不能为空");
return errors;
}
public List<String> validateReview(ProductReview review) {
List<String> errors = new ArrayList<>();
if (review == null) { errors.add("评价信息为空"); return errors; }
if (review.getRating() < 1 || review.getRating() > 5)
errors.add("评分必须在 1-5 之间");
String sentiment = review.getSentiment();
if (sentiment == null || !(sentiment.equals("positive") ||
sentiment.equals("neutral") || sentiment.equals("negative")))
errors.add("情感倾向必须是 positive/neutral/negative 之一");
if (review.getKeyPoints() == null || review.getKeyPoints().length == 0)
errors.add("关键点不能为空");
return errors;
}
public boolean isValid(List<String> errors) {
return errors == null || errors.isEmpty();
}
}
4.4 Agent 配置类(整合 Rules 和 Skills)
package com.example.ai.config;
import com.alibaba.cloud.ai.graph.agent.ReactAgent;
import com.alibaba.cloud.ai.graph.checkpoint.savers.MemorySaver;
import com.example.ai.model.ContactInfo;
import com.example.ai.model.ProductReview;
import com.example.ai.harness.skills.ReviewAnalysisSkill;
import org.springframework.ai.chat.model.ChatModel;
import org.springframework.ai.converter.BeanOutputConverter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.ClassPathResource;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
@Configuration
public class HarnessAgentConfig {
private final ReviewAnalysisSkill reviewAnalysisSkill;
public HarnessAgentConfig(ReviewAnalysisSkill reviewAnalysisSkill) {
this.reviewAnalysisSkill = reviewAnalysisSkill;
}
@Bean
public ReactAgent contactAgent(ChatModel chatModel) throws IOException {
String rules = loadRules("harness/rules/ExtractionRules.md");
String systemPrompt = "你是一个联系人信息提取专家。请严格遵循以下规则:\n" + rules;
return ReactAgent.builder()
.name("contact_extractor")
.model(chatModel)
.systemPrompt(systemPrompt)
.outputType(ContactInfo.class)
.saver(new MemorySaver()) // 状态管理
.build();
}
@Bean
public ReactAgent reviewAgent(ChatModel chatModel) {
BeanOutputConverter<ProductReview> converter =
new BeanOutputConverter<>(ProductReview.class);
String schema = converter.getFormat();
return ReactAgent.builder()
.name("review_analyzer")
.model(chatModel)
.systemPrompt("你是一个商品评价分析专家,必须按以下 JSON Schema 格式输出:\n" + schema)
.outputSchema(schema)
.saver(new MemorySaver())
.build();
}
private String loadRules(String path) throws IOException {
ClassPathResource resource = new ClassPathResource(path);
return new String(resource.getInputStream().readAllBytes(), StandardCharsets.UTF_8);
}
}
4.5 Service 层(集成 Gate 校验)
package com.example.ai.service;
import com.alibaba.cloud.ai.graph.RunnableConfig;
import com.alibaba.cloud.ai.graph.agent.ReactAgent;
import com.alibaba.cloud.ai.graph.exception.GraphRunnerException;
import com.example.ai.model.ContactInfo;
import com.example.ai.model.ProductReview;
import com.example.ai.harness.gates.OutputValidator;
import com.example.ai.harness.skills.ReviewAnalysisSkill;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.ai.chat.messages.AssistantMessage;
import org.springframework.stereotype.Service;
import java.util.List;
@Service
public class HarnessAgentService {
private final ReactAgent contactAgent;
private final ReactAgent reviewAgent;
private final ReviewAnalysisSkill reviewAnalysisSkill;
private final OutputValidator outputValidator;
private final ObjectMapper objectMapper;
public HarnessAgentService(ReactAgent contactAgent, ReactAgent reviewAgent,
ReviewAnalysisSkill reviewAnalysisSkill,
OutputValidator outputValidator,
ObjectMapper objectMapper) {
this.contactAgent = contactAgent;
this.reviewAgent = reviewAgent;
this.reviewAnalysisSkill = reviewAnalysisSkill;
this.outputValidator = outputValidator;
this.objectMapper = objectMapper;
}
public ContactInfo extractContact(String text, String sessionId) {
RunnableConfig config = RunnableConfig.builder().threadId(sessionId).build();
AssistantMessage response;
try {
response = contactAgent.call(text, config);
} catch (GraphRunnerException e) {
throw new RuntimeException("Agent 执行失败: " + e.getMessage(), e);
}
String json = response.getText();
ContactInfo contact;
try {
contact = objectMapper.readValue(json, ContactInfo.class);
} catch (Exception e) {
throw new RuntimeException("解析结构化输出失败,原始 JSON: " + json, e);
}
// === Gate 门禁校验 ===
List<String> errors = outputValidator.validateContact(contact);
if (!outputValidator.isValid(errors)) {
throw new RuntimeException("输出校验失败: " + String.join("; ", errors));
}
return contact;
}
public ProductReview analyzeReview(String reviewText, String sessionId) {
RunnableConfig config = RunnableConfig.builder().threadId(sessionId).build();
String prompt = reviewAnalysisSkill.buildPrompt(reviewText);
AssistantMessage response;
try {
response = reviewAgent.call(prompt, config);
} catch (GraphRunnerException e) {
throw new RuntimeException("Agent 执行失败: " + e.getMessage(), e);
}
String json = response.getText();
ProductReview review;
try {
review = objectMapper.readValue(json, ProductReview.class);
} catch (Exception e) {
throw new RuntimeException("解析结构化输出失败,原始 JSON: " + json, e);
}
// === Skill 后处理 ===
review = reviewAnalysisSkill.postProcess(review);
// === Gate 门禁校验 ===
List<String> errors = outputValidator.validateReview(review);
if (!outputValidator.isValid(errors)) {
throw new RuntimeException("输出校验失败: " + String.join("; ", errors));
}
return review;
}
// 用于调试,跳过 Gate
public String extractContactRaw(String text, String sessionId) {
RunnableConfig config = RunnableConfig.builder().threadId(sessionId).build();
try {
AssistantMessage response = contactAgent.call(text, config);
return response.getText();
} catch (GraphRunnerException e) {
throw new RuntimeException("Agent 执行失败: " + e.getMessage(), e);
}
}
}
4.6 Controller 层
package com.example.ai.controller;
import com.example.ai.model.ContactInfo;
import com.example.ai.model.ProductReview;
import com.example.ai.service.HarnessAgentService;
import org.springframework.web.bind.annotation.*;
import java.util.Map;
@RestController
@RequestMapping("/api/harness")
public class HarnessController {
private final HarnessAgentService service;
public HarnessController(HarnessAgentService service) {
this.service = service;
}
@PostMapping("/contact")
public Map<String, Object> extractContact(
@RequestParam String text,
@RequestParam(required = false, defaultValue = "default") String sessionId) {
ContactInfo contact = service.extractContact(text, sessionId);
return Map.of("success", true, "data", contact, "sessionId", sessionId);
}
@PostMapping("/contact/raw")
public Map<String, Object> extractContactRaw(
@RequestParam String text,
@RequestParam(required = false, defaultValue = "default") String sessionId) {
String json = service.extractContactRaw(text, sessionId);
return Map.of("success", true, "json", json, "sessionId", sessionId);
}
@PostMapping("/review")
public Map<String, Object> analyzeReview(
@RequestParam String reviewText,
@RequestParam(required = false, defaultValue = "default") String sessionId) {
ProductReview review = service.analyzeReview(reviewText, sessionId);
return Map.of("success", true, "data", review, "sessionId", sessionId);
}
}
4.7 启动类
package com.example.ai;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class SpringAiHarnessDemoApplication {
public static void main(String[] args) {
SpringApplication.run(SpringAiHarnessDemoApplication.class, args);
}
}
五、运行与测试
5.1 启动应用
在项目根目录(pom.xml 所在目录)打开命令行:
mvn clean package
java -jar target/spring-ai-harness-demo-1.0.0.jar
或者直接在 IDEA 中运行 SpringAiHarnessDemoApplication 主类。
看到日志输出 Started SpringAiHarnessDemoApplication 即表示启动成功。
5.2 测试联系人提取
打开另一个命令行窗口,执行:
curl -X POST "http://localhost:885/api/harness/contact?text=从以下信息提取联系方式:王五,wangwu@outlook.com,+86 139-9999-8888&sessionId=test01"
预期返回(示例):
{
"success": true,
"data": {
"name": "王五",
"email": "wangwu@outlook.com",
"phone": "+86 139-9999-8888"
},
"sessionId": "test01"
}
5.3 测试商品评价分析
curl -X POST "http://localhost:885/api/harness/review?reviewText=这款耳机音质不错,降噪效果好,但佩戴舒适度一般,价格略高。&sessionId=test02"
预期返回:
{
"success": true,
"data": {
"rating": 4,
"sentiment": "positive",
"keyPoints": ["音质不错", "降噪效果好", "佩戴舒适度一般", "价格略高"],
"details": {
"pros": ["音质不错", "降噪效果好"],
"cons": ["佩戴舒适度一般", "价格略高"],
"summary": "整体满意,舒适度和价格有改进空间"
}
},
"sessionId": "test02"
}
5.4 测试原始 JSON(跳过 Gate)
curl -X POST "http://localhost:885/api/harness/contact/raw?text=张伟,zhangwei@163.com,010-88886666&sessionId=test03"
这将返回未经校验的原始 JSON,便于调试。
六、Harness Engineering 在本示例中的完整体现
| Harness 组件 | 实现位置 | 作用 |
|---|---|---|
| Rules(规则) | ExtractionRules.md |
软约束,告诉 Agent 必须提取哪些字段、禁止编造 |
| Skills(技能) | ReviewAnalysisSkill.java |
半硬约束,封装了评价分析的标准化提示词和后处理逻辑 |
| Gate(门禁) | OutputValidator.java |
硬约束,强制校验输出结构,不通过则抛出异常 |
| State(状态) | MemorySaver + threadId |
管理会话上下文,同一 sessionId 可保持多轮记忆 |
| Instructions(指令) | systemPrompt 和 outputType |
明确告诉 Agent 输出格式和行为边界 |
| Verification(验证) | 异常处理 + Gate 校验 | 只有通过全部校验才算任务成功完成 |
为什么“仅靠代码层面”就能实现全部功能?
因为Harness Engineering所依赖的组件,在成熟的Java框架(Spring AI Alibaba)和编程语言特性中原生就存在。你不需要安装额外的东西,只需要按照Harness的思想去编排这些现有组件:
| Harness 组件 | 在这个示例中的“代码级”实现 | 为什么不需要额外安装? |
|---|---|---|
| Rules(规则) | 写了一个 .md 文件,用 @Bean 注入到 systemPrompt 里 |
因为Prompt本身就是字符串,读取文件是Java原生IO能力,无需外部引擎。 |
| Skills(技能) | 写了一个普通的Spring @Component 类,里面封装了buildPrompt方法 |
这就是标准的Java对象(POJO),利用OOP封装复用逻辑,不需要脚本引擎。 |
| Gate(门禁) | 写了一个 OutputValidator 类,用 if 判断字段是否为空、格式是否正确 |
这就是最传统的Java Bean Validation(类似 @NotNull),是业务代码的一部分。 |
| State(状态) | 直接用了框架自带的 MemorySaver() |
Spring AI Alibaba 已经内置了记忆管理模块,你只需调用 new MemorySaver() 就行,不需要自己搭建Redis或数据库来做这件事。 |
| Instructions(指令) | 配置了 outputType(ContactInfo.class) |
这是利用了Spring AI的结构化输出能力,它底层帮你在Prompt里拼装了JSON Schema,这也是代码层面的转换。 |
3. 打个比方帮助你理解
- Harness Engineering(方法论) 相当于“营养学原理”(要补充蛋白质、维生素)。
- 你写的Spring Boot代码(本例) 相当于“自己下厨做营养餐”(利用现有的蔬菜和肉,遵循营养学原理搭配)。
在你提供的这个示例中,我们选择了“自己下厨”。因为我们已有的食材(Spring Boot、Jackson、Java 17)本身就足够强大,我们只需要按照Harness Engineering的食谱(方法论) 把它们组合起来:
- 用
ObjectMapper解析JSON(代替了Gate工具) - 用
try-catch处理异常(代替了错误恢复工具) - 用
threadId区分会话(代替了分布式会话管理软件)
七、总结
通过本示例,我们完成了:
- 理解 Harness Engineering:将其核心组件(Rules、Skills、Gate)落地到真实代码中。
- Windows 本地环境搭建:从 JDK、Maven 到 API Key 配置,确保可复现。
- 完整代码实现:利用 Spring AI Alibaba 的结构化输出能力,构建了两个带“马具”的 Agent——联系人提取和商品评价分析。
- 运行与测试:通过 curl 验证了 Agent 的可靠输出和 Gate 的强制校验。
Harness Engineering 的精髓在于将 AI 的不确定性通过工程手段转化为确定性。它让开发者不再被动地“提示”模型,而是主动地“驾驭”模型。当模型能力趋于同质化时,Harness 的质量决定了产品的最终体验和商业成功。
下一步,你可以尝试:
- 为 Agent 添加更多工具(如数据库查询、Web 搜索);
- 将 Rules 升级为动态加载,实现热更新;
- 集成更复杂的 Skill 流程,实现多步骤任务。
参考资源:
更多推荐
所有评论(0)