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 JDKOpenJDK
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 -versionmvn -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(指令) systemPromptoutputType 明确告诉 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 区分会话(代替了分布式会话管理软件)

七、总结

通过本示例,我们完成了:

  1. 理解 Harness Engineering:将其核心组件(Rules、Skills、Gate)落地到真实代码中。
  2. Windows 本地环境搭建:从 JDK、Maven 到 API Key 配置,确保可复现。
  3. 完整代码实现:利用 Spring AI Alibaba 的结构化输出能力,构建了两个带“马具”的 Agent——联系人提取和商品评价分析。
  4. 运行与测试:通过 curl 验证了 Agent 的可靠输出和 Gate 的强制校验。

Harness Engineering 的精髓在于将 AI 的不确定性通过工程手段转化为确定性。它让开发者不再被动地“提示”模型,而是主动地“驾驭”模型。当模型能力趋于同质化时,Harness 的质量决定了产品的最终体验和商业成功。

下一步,你可以尝试:

  • 为 Agent 添加更多工具(如数据库查询、Web 搜索);
  • 将 Rules 升级为动态加载,实现热更新;
  • 集成更复杂的 Skill 流程,实现多步骤任务。

参考资源

Logo

这里是“一人公司”的成长家园。我们提供从产品曝光、技术变现到法律财税的全栈内容,并连接云服务、办公空间等稀缺资源,助你专注创造,无忧运营。

更多推荐