【AgentScope Java新手村系列】(2)第一个Agent-基础对话

第二章 HarnessAgent 与流式对话:用 DeepSeek 跑通第一个会思考的 Agent

"ReAct(推理-行动)是 Agent 框架的核心范式。本章用 OpenAIChatModel 接入 DeepSeek,演示 HarnessAgent 的 Builder 模式、流式事件、思考模式三个关键能力------从此 LLM 真正'活'起来。"

2.1 最简示例

这一章从零开始构建一个能对话的 Agent。完整代码如下:

复制代码
package com.example;

import io.agentscope.core.ReActAgent;
import io.agentscope.core.agent.RuntimeContext;
import io.agentscope.core.formatter.openai.OpenAIChatFormatter;
import io.agentscope.core.message.UserMessage;
import io.agentscope.core.model.GenerateOptions;
import io.agentscope.core.model.OpenAIChatModel;
import io.agentscope.core.tool.Toolkit;
import io.agentscope.harness.HarnessAgent;

import java.nio.file.Path;

public class BasicChatExample {
    public static void main(String[] args) {
        String apiKey = System.getenv("DEEPSEEK_API_KEY");

        // 创建 Model
        OpenAIChatModel model = OpenAIChatModel.builder()
                .apiKey(apiKey)
                .modelName("deepseek-chat")
                .baseUrl("https://api.deepseek.com")
                .stream(true)
                .enableThinking(true)
                .formatter(new OpenAIChatFormatter())
                .defaultOptions(GenerateOptions.builder()
                        .thinkingBudget(1024)
                        .build())
                .build();

        // 方式 A:纯 ReActAgent(最轻量,仅一个推理循环)
        ReActAgent plain = ReActAgent.builder()
                .name("Assistant")
                .sysPrompt("你是一个乐于助人的AI助手,请友好简洁地回答问题。")
                .model(model)
                .toolkit(new Toolkit())
                .build();

        // 方式 B:HarnessAgent(推荐------开箱即用:工作区、Session、记忆、子 agent、压缩...)
        HarnessAgent agent = HarnessAgent.builder()
                .name("Assistant")
                .sysPrompt("你是一个乐于助人的AI助手,请友好简洁地回答问题。")
                .model(model)
                .workspace(Path.of("./workspace"))
                .build();

        // 构造用户消息 ------ 2.0 推荐用具体子类型
        UserMessage userMsg = new UserMessage("你好,请介绍一下自己");

        // 调用 Agent 并获取回复
        //  - streamEvents() 是 2.0 推荐的流式 API(返回 Flux<AgentEvent>)
        //  - call() 是简化的同步入口(返回 Mono<Msg>)
        String reply = agent.call(userMsg, RuntimeContext.empty())
                .block()
                .getTextContent();
        System.out.println(reply);
    }
}

2.2 代码拆解

创建 Model

复制代码
OpenAIChatModel.builder()
        .apiKey(apiKey)                    // API Key
        .modelName("deepseek-chat")        // 模型名称
        .baseUrl("https://api.deepseek.com")
        .stream(true)                      // 启用流式输出
        .enableThinking(true)              // 启用思考模式(类似 DeepSeek 的思考链)
        .formatter(new OpenAIChatFormatter())  // 格式化器,负责将消息转换为 API 格式
        .defaultOptions(GenerateOptions.builder()
                .thinkingBudget(1024)       // 思考 token 预算
                .build())
        .build()

OpenAIChatModel 使用 Builder 模式创建。关键配置:

  • apiKey:LLM 服务的 API Key(DeepSeek 用 DEEPSEEK_API_KEY,OpenAI 用 OPENAI_API_KEY
  • modelName:模型标识,DeepSeek 用 deepseek-chatdeepseek-reasoner,OpenAI 用 gpt-4o
  • stream:是否使用流式输出
  • enableThinking:是否启用思考模式,启用后 Agent 会先"思考"再回答
  • formatter:格式化器,不同模型提供商有不同的格式化器

创建 Agent ------ ReActAgent vs HarnessAgent

2.0 有两个入口类,按场景二选一:

复制代码
// 纯 ReActAgent:一个推理循环,无任何工程能力
ReActAgent.builder()
        .name("Assistant")
        .sysPrompt("你是一个乐于助人的AI助手,请友好简洁地回答问题。")
        .model(model)
        .toolkit(new Toolkit())
        .build();

// HarnessAgent(推荐):在 ReActAgent 之上叠加了工作区、Session、记忆、子 agent、压缩...
// 不开任何额外能力时行为等价于裸 ReActAgent;按需打开:
HarnessAgent.builder()
        .name("Assistant")
        .sysPrompt("你是一个乐于助人的AI助手,请友好简洁地回答问题。")
        .model(model)
        .workspace(Path.of("./workspace"))      // 必填:Harness 需要工作区根目录
        // .stateStore(...)                     // 可选:分布式 AgentStateStore 后端(Redis/MySQL...)
        // .compaction(...)                      // 可选:上下文压缩
        // .subagent(...)                        // 可选:声明子 agent
        // .skillRepository(...)                 // 可选:技能仓库
        // .enablePlanMode()                     // 可选:Plan Mode(HITL 退出)
        .build();

对比要点

维度 ReActAgent HarnessAgent
推理循环 ✅(继承)
工具调用
会话持久化 ❌(1.x 靠 Memory,2.0 需自己接 Session ✅ 默认 WorkspaceSession,可换 Redis/MySQL
工作区 / 长期记忆
上下文压缩 ✅ 按需打开
子 agent 编排
沙箱隔离
适用场景 学习 ReAct 原理、写单次脚本 生产/长期运行的 agent

构造消息

2.0 推荐用具体子类型而不是通用 Msg.builder()

复制代码
import io.agentscope.core.message.UserMessage;

UserMessage userMsg = new UserMessage("你好,请介绍一下自己");

四种角色都有对应子类型:UserMessage / AssistantMessage / SystemMessage / ToolResultMessage。每个子类型都有便捷构造函数和 Builder,可以附加多模态块(TextBlock / DataBlock / ToolUseBlock / ToolResultBlock)。

如果需要更细的控制(比如多块、命名发送方),仍可以用 Msg.builder()

复制代码
import io.agentscope.core.message.Msg;
import io.agentscope.core.message.MsgRole;
import io.agentscope.core.message.TextBlock;

Msg userMsg = Msg.builder()
        .name("alice")
        .role(MsgRole.USER)
        .textContent("你好")                  // 单文本快捷方式
        .build();
// 等价于:
Msg userMsg2 = Msg.builder()
        .name("alice")
        .role(MsgRole.USER)
        .content(TextBlock.builder().text("你好").build())
        .build();

调用 Agent

复制代码
import io.agentscope.core.agent.RuntimeContext;

String reply = agent.call(new UserMessage("你好"), RuntimeContext.empty())
        .block()
        .getTextContent();
  • agent.call(messages, ctx) 返回 Mono<Msg>(Project Reactor 的响应式类型),.block() 将异步操作转为同步等待。
  • RuntimeContext.empty() 是不带任何身份信息的"裸"上下文。生产环境里至少填 sessionIduserId,详见第五章。
  • 2.0 起,多个 call() 之间通过同一个 sessionId 自动恢复历史;不再需要手动 memory.add(msg)

没有 RuntimeContext 这个参数的方法 也存在(1.x 风格的 agent.call(msg)),但新代码请统一传 RuntimeContext------所有 middleware、hook、tool 都靠它读"这是谁在说话"。漏传会让 Session 持久化、权限规则、人机交互等全部失效。

2.3 多轮对话

会话历史由 Session 自动管理。每次调用 call() 时,只要 RuntimeContext 里的 sessionId 不变,就会自动接上次的上下文。

复制代码
package com.example;

import io.agentscope.core.ReActAgent;
import io.agentscope.core.agent.RuntimeContext;
import io.agentscope.core.formatter.openai.OpenAIChatFormatter;
import io.agentscope.core.message.UserMessage;
import io.agentscope.core.model.OpenAIChatModel;
import io.agentscope.core.tool.Toolkit;
import io.agentscope.harness.HarnessAgent;

import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.nio.file.Path;

public class MultiTurnChat {
    public static void main(String[] args) throws Exception {
        String apiKey = System.getenv("DEEPSEEK_API_KEY");
        BufferedReader reader = new BufferedReader(new InputStreamReader(System.in));

        OpenAIChatModel model = OpenAIChatModel.builder()
                .apiKey(apiKey)
                .modelName("deepseek-chat")
                .baseUrl("https://api.deepseek.com")
                .stream(true)
                .formatter(new OpenAIChatFormatter())
                .build();

        HarnessAgent agent = HarnessAgent.builder()
                .name("Assistant")
                .sysPrompt("你是一个乐于助人的AI助手,请友好简洁地回答问题。")
                .model(model)
                .workspace(Path.of("./workspace"))
                .build();

        System.out.println("=== Chat Started ===");
        System.out.println("Type 'exit' to quit\n");

        while (true) {
            System.out.print("You> ");
            String input = reader.readLine();

            if (input == null || "exit".equalsIgnoreCase(input.trim())) {
                System.out.println("Goodbye!");
                break;
            }

            if (input.trim().isEmpty()) {
                continue;
            }

            // 同一 sessionId 走同一份历史;这里是 demo 用固定值
            RuntimeContext ctx = RuntimeContext.builder()
                    .sessionId("demo-001")
                    .userId("alice")
                    .build();

            String reply = agent.call(new UserMessage(input), ctx)
                    .block()
                    .getTextContent();
            System.out.println("Agent> " + reply + "\n");
        }
    }
}

运行后可以进行多轮对话,Agent 会记住之前的对话内容。sessionId 是恢复的"钥匙"------同一个 sessionId 不管在哪个节点调用,都会拼回同一段历史。

2.4 切换模型

框架内置支持多种模型,切换只需更换 Model 实现。

使用 OpenAI

复制代码
import io.agentscope.core.model.OpenAIChatModel;

OpenAIChatModel model = OpenAIChatModel.builder()
        .apiKey(System.getenv("OPENAI_API_KEY"))
        .modelName("gpt-4o")
        .stream(true)
        .build();

使用 Anthropic Claude

复制代码
import io.agentscope.core.model.AnthropicChatModel;

OpenAIChatModel model = AnthropicChatModel.builder()
        .apiKey(System.getenv("ANTHROPIC_API_KEY"))
        .modelName("claude-sonnet-4-20250514")
        .build();

使用 Google Gemini

复制代码
import io.agentscope.core.model.GeminiChatModel;

OpenAIChatModel model = GeminiChatModel.builder()
        .apiKey(System.getenv("GEMINI_API_KEY"))
        .modelName("gemini-2.0-flash")
        .build();

使用 Ollama(本地模型)

复制代码
// 2.0 不再有独立的 OllamaChatModel;用 OpenAIChatModel + Ollama 的 OpenAI 兼容端点
OpenAIChatModel model = OpenAIChatModel.builder()
        .apiKey("ollama")                   // Ollama 不校验 key
        .modelName("llama3")
        .baseUrl("http://localhost:11434")
        .build();

Ollama 不需要 API Key,但需要在本地运行 Ollama 服务。

使用 OpenAI 兼容接口

很多国产模型提供 OpenAI 兼容接口,可以通过 baseUrl 配置:

复制代码
OpenAIChatModel.builder()
        .apiKey("your-api-key")
        .baseUrl("https://your-model-endpoint.com/v1/")
        .modelName("your-model-name")
        .build()

2.5 GenerateOptions 生成参数

GenerateOptions 控制 LLM 的生成行为:

复制代码
import io.agentscope.core.model.GenerateOptions;

GenerateOptions options = GenerateOptions.builder()
        .temperature(0.7)        // 温度,控制随机性,0-2
        .topP(0.9)               // 核采样参数
        .maxTokens(2048)         // 最大输出 token 数
        .frequencyPenalty(0.0)   // 频率惩罚
        .presencePenalty(0.0)    // 存在惩罚
        .build();

// 作为 Model 的默认参数
OpenAIChatModel.builder()
        .apiKey(apiKey)
        .modelName("deepseek-chat")
        .baseUrl("https://api.deepseek.com")
        .defaultOptions(options)
        .build();

调用时也可以覆盖:

复制代码
agent.call(userMsg, ctx, options).block();

2.6 流式输出

2.0 有两套流式 API:

API 返回类型 状态 适用场景
agent.streamEvents(messages, ctx) Flux<AgentEvent> 推荐 新代码;只关心父 agent 自身事件(文本增量、工具调用、生命周期)
agent.stream(messages, opts, ctx) Flux<Event> @Deprecated(forRemoval = true) 目前唯一 能实时拿到子 agent 事件的入口;待 AgentEvent 子来源通道落地后也将迁移

AgentEvent 体系的关键事件类型(io.agentscope.core.event 包下):

事件 含义
AgentStartEvent / AgentEndEvent 一次 call() 的开始/结束
TextBlockDeltaEvent 文本增量(一个 token / 一段 chunk)
ReasoningEvent 思考过程(启用 enableThinking 后才有)
ToolCallStartEvent / ToolResultStartEvent 工具调用开始/工具结果开始
RequireUserConfirmEvent 需要用户确认(人机交互)

2.6.1 streamEvents() ------ 2.0 推荐

复制代码
import io.agentscope.core.event.AgentEvent;
import io.agentscope.core.event.AgentEventType;
import io.agentscope.core.event.TextBlockDeltaEvent;
import io.agentscope.core.event.ToolCallStartEvent;
import io.agentscope.core.message.UserMessage;
import io.agentscope.core.agent.RuntimeContext;

agent.streamEvents(new UserMessage("写一首关于秋天的诗"), RuntimeContext.empty())
        .doOnNext(event -> {
            if (event.getType() == AgentEventType.TEXT_BLOCK_DELTA) {
                System.out.print(((TextBlockDeltaEvent) event).getDelta());
            } else if (event.getType() == AgentEventType.TOOL_CALL_START) {
                ToolCallStartEvent start = (ToolCallStartEvent) event;
                System.out.println("\n[tool] " + start.getToolName());
            }
        })
        .blockLast();

streamEvents() 这条路径不会 把子 agent 事件转发出来------子 agent 在静默运行,结果以 TOOL_RESULT 块的形式回给父 agent。详见第七章。

2.6.2 stream() ------ 已弃用但目前唯一能拿到子 agent 事件的入口

复制代码
import io.agentscope.core.agent.Event;
import io.agentscope.core.agent.EventType;
import io.agentscope.core.agent.StreamOptions;
import io.agentscope.core.message.UserMessage;
import io.agentscope.core.agent.RuntimeContext;
import reactor.core.publisher.Flux;

StreamOptions streamOptions = StreamOptions.builder()
        .eventTypes(EventType.REASONING, EventType.TOOL_RESULT, EventType.AGENT_RESULT)
        .incremental(true)                  // 增量 vs 累积
        .build();

Flux<Event> events = agent.stream(
        List.of(new UserMessage("写一首关于秋天的诗")),
        streamOptions,
        RuntimeContext.empty());

events.doOnNext(event -> {
    String text = event.getMessage() != null ? event.getMessage().getTextContent() : null;
    if (text != null && !text.isEmpty()) {
        System.out.print(text);
    }
}).blockLast();

StreamOptions 的配置:

  • eventTypes:订阅的事件类型(REASONING 思考过程、TOOL_RESULT 工具结果、AGENT_RESULT 最终结果)
  • incrementaltrue 表示增量输出(每次只输出新内容),false 表示累积输出(每次输出从头到当前的全部内容)

之所以保留 stream() 是因为它是目前 唯一能在父流里实时看到子 agent 事件的入口。在 AgentEvent 子来源通道落地后,stream() 也会彻底退役。新写代码默认 streamEvents(),需要子 agent 事件时再切 stream()

2.7 Msg 的更多用法

2.7.1 快捷创建(具体子类型)

复制代码
import io.agentscope.core.message.UserMessage;
import io.agentscope.core.message.SystemMessage;
import io.agentscope.core.message.AssistantMessage;
import io.agentscope.core.message.TextBlock;

UserMessage userMsg = new UserMessage("你好!");                       // 用户消息
SystemMessage sysMsg = new SystemMessage("你是一个乐于助人的助手。");  // 系统消息
AssistantMessage asstMsg = new AssistantMessage("你好!有什么可以帮你的吗?");            // Agent 回复

2.7.2 通用 Msg.builder()

复制代码
import io.agentscope.core.message.Msg;
import io.agentscope.core.message.MsgRole;

// 最简写法(默认 USER 角色)
Msg msg = Msg.builder().textContent("你好").build();

// 指定角色
Msg systemMsg = Msg.builder()
        .role(MsgRole.SYSTEM)
        .textContent("你是一个乐于助人的助手。")
        .build();

2.7.3 读取消息内容

复制代码
import io.agentscope.core.message.ContentBlock;
import java.util.List;

Msg response = agent.call(userMsg, ctx).block();

// 获取文本内容
String text = response.getTextContent();

// 获取角色
MsgRole role = response.getRole();

// 获取发送者名称
String name = response.getName();

// 获取所有内容块
List<ContentBlock> blocks = response.getContent();

2.7.4 MsgRole 枚举

复制代码
public enum MsgRole {
    USER,       // 用户消息
    ASSISTANT,  // Agent 回复
    SYSTEM,     // 系统消息(如系统提示词)
    TOOL        // 工具调用结果
}
相关推荐
ZeroNews内网穿透1 小时前
NAS部署Hermes AI Agent + 零讯内网穿透,实现远程可管理的AI助手
人工智能·安全·ai·内网穿透
摇滚侠1 小时前
Spring MVC 不是一个单独的框架,是 Spring 框架的一个模块
java·spring·mvc
阿正的梦工坊1 小时前
【Rust】04-借用、引用与切片
java·数据库·rust
小二·1 小时前
Spring Boot 3 + Vue 3 全栈开发实战
vue.js·spring boot·后端
devilnumber2 小时前
静态代理 & 动态代理:实战运用 + 场景区别 + 怎么选
java·开发语言·代理模式
码农飞哥2 小时前
Spring Boot 多角色权限隔离实战:接口层+路由层+UI层三层防御,杜绝生产数据泄露
spring boot·状态模式·架构设计·系统设计·权限控制
Upsy-Daisy2 小时前
Hermes Agent 学习笔记 02:安装、配置与第一次运行
java·前端·数据库
SuperArc19992 小时前
SpringBoot+Slf4j+Log4j2+mybatis 日志整合
spring boot·mybatis·log4j2·slf4j·日志整合
用户4682557459132 小时前
Testcontainers 在 Windows Docker Desktop 上跑不通:协议层不兼容 + 4 种可行环境
java·后端