第二章 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-chat或deepseek-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()是不带任何身份信息的"裸"上下文。生产环境里至少填sessionId和userId,详见第五章。- 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最终结果)incremental:true表示增量输出(每次只输出新内容),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 // 工具调用结果
}