1.1 固定工作流的局限性
在上一篇文章中,学习了如何为重复性工作构建固定的工作流。假设你需要为公司开发了一个「课程前期调研」机器人,它有一个固定的流程:当收到调研需求时,并行分析用户画像、竞品课程和行业需求。
现在,一位课程设计师向机器人发出了指令:「请帮我完成一门新的 Python 入门课程的前期调研。」
机器人忠实地启动了你预设的工作流:
- 行业需求分析子任务:调用工具,成功。
- 用户画像定义子任务:调用工具,成功。
- 竞品课程分析子任务:调用
analyze_competitor_course工具,却收到了一个错误:「错误:因竞品网站布局更新,无法解析课程大纲。」
这时,你的机器人将无法继续执行。因为它被设计的流程里,没有处理「竞品分析工具失效」这个意外情况的步骤。
3.2 朴素解法:增加新的分支
你会想,只要为工作流添加异常处理分支就可以了。你可以在原有的固定流程里增加一个分支:如果 analyze_competitor_course 失败,那就执行一个新的步骤,比如提醒课程设计师人工处理。
这种「打补丁」的方案看似有效,但如果下一次是行业需求分析的 API 临时维护失败了呢?如果用户画像数据源格式变更了呢?你永远无法预知所有可能的意外;分支树会无限膨胀,且仍会在「未预料到的异常」上卡住。
扩展阅读:目标 (Goal) vs. 任务 (Task)
- 目标 (Goal):用户希望达成的最终状态,例如「完成 Python 入门课的调研」。
-
任务 (Task):具体动作,例如「调用
analyze_competitor_course,参数{url: ...}」。
固定工作流 Agent 处理的是「任务」;当任务失败时往往无能为力。更智能的 Agent 应围绕「目标」重新规划新的任务序列。
3.3 让 Agent 学会自主规划(Planning)
开发者的角色从「流程设计师」转为「目标设定者 + 工具提供者」;Agent 从「任务执行者」升级为「解决方案规划师」:
- 接收目标
- 动态规划:LLM 分解目标,生成可执行的 Plan(在 AgentScope 中由 PlanNotebook 与内置计划元工具承载)
- 执行计划:执行器按计划调用业务工具,并在失败时调整子任务
Java:DashScope-java 模型构建(AgentScopeDashScopeModels.java 全文)
Java:PlanNotebook + 课程调研单测(CourseResearcherPlanNotebookAgentScopeTest.java 全文)
package com.baoma.ai.debug;
import com.baoma.ai.debug.support.AgentScopeDashScopeModels;
import io.agentscope.core.ReActAgent;
import io.agentscope.core.memory.InMemoryMemory;
import io.agentscope.core.message.Msg;
import io.agentscope.core.plan.PlanNotebook;
import io.agentscope.core.plan.model.Plan;
import io.agentscope.core.plan.model.SubTask;
import io.agentscope.core.plan.storage.InMemoryPlanStorage;
import io.agentscope.core.tool.Tool;
import io.agentscope.core.tool.ToolParam;
import io.agentscope.core.tool.Toolkit;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Assumptions;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.CopyOnWriteArrayList;
/**
* 演示:启用 PlanNotebook 后,模型可 create_plan / finish_subtask 等;
* 业务工具里竞品解析固定失败,引导模型改用搜索与 PDF 工具完成「目标」而非卡死在单一步骤。
*/
class CourseResearcherPlanNotebookAgentScopeTest {
/** 在 PlanNotebook 变更钩子里追加可读快照,便于观察计划演化。 */
private static final List<String> PLAN_SNAPSHOT_LINES = new CopyOnWriteArrayList<>();
@Test
@DisplayName("AgentScope:PlanNotebook 钩子 + 课程调研工具(竞品失败后绕行)")
void planNotebookCourseResearchDemo() {
// 多变量兜底,未配置密钥时 Assumptions 跳过而非失败(CI 友好)
String apiKey = AgentScopeDashScopeModels.firstNonBlank(
System.getenv("AGENTSCOPE_DEBUG_API_KEY"),
System.getenv("REASONING_DEBUG_API_KEY"),
System.getenv("DASHSCOPE_API_KEY")
);
Assumptions.assumeTrue(apiKey != null && !apiKey.isBlank(),
"请配置 DASHSCOPE_API_KEY(或 AGENTSCOPE_DEBUG_API_KEY 等)后重跑");
String modelName = AgentScopeDashScopeModels.firstNonBlank(
System.getenv("AGENTSCOPE_COURSE_PLAN_MODEL"),
"qwen-max"
);
PLAN_SNAPSHOT_LINES.clear();
// 业务工具:竞品失败 + 行业需求 + 搜索 + PDF,与课件语义一致
Toolkit toolkit = new Toolkit();
toolkit.registerTool(new CourseResearchTools());
// 注册计划相关元工具(create_plan、finish_subtask 等),与 enablePlan() 配合使用
toolkit.registerMetaTool();
// PlanNotebook:计划状态与存储核心;create_plan / finish_subtask 等元工具读写的就是本实例。
// builder() —— 链式配置计划本。
// .storage(InMemoryPlanStorage) —— 计划存进程内内存,单测/演示用;生产可换可持久化 PlanStorage。
// .maxSubtasks(24) —— 单份计划子任务个数上限,防止模型无限拆步撑爆上下文。
// .needUserConfirm(false) —— 框架侧尽量不强制「用户对计划变更点确认」;模型仍可能口头问「是否开始」。
// .build() —— 生成实例,再交给 ReActAgent.builder().planNotebook(...).enablePlan()。
PlanNotebook planNotebook = PlanNotebook.builder()
.storage(new InMemoryPlanStorage())
.maxSubtasks(24)
.needUserConfirm(false)
.build();
// 计划变更时回调:把当前 Plan 打成多行文本,相当于「可观测的计划快照」
planNotebook.addChangeHook("capture", (notebook, plan) -> {
if (plan == null) {
return;
}
PLAN_SNAPSHOT_LINES.add(renderPlanSnapshot(plan));
});
var model = AgentScopeDashScopeModels.buildDashScopeChatModel(apiKey, modelName);
// enablePlan:把 PlanNotebook 接到 ReAct 循环;memory 避免多轮丢上下文;maxIters 防止死循环耗光配额
ReActAgent agent = ReActAgent.builder()
.name("CourseResearcherAgent")
.sysPrompt("""
你是课程调研助手。遇到复杂任务时:
0. 不要向用户索要「是否开始」「请回复是」等确认;收到任务后立刻 create_plan 并执行,直至 finish_plan。
1. 使用内置计划能力:先 create_plan 列出子任务,再逐步执行;
2. 执行过程中用 finish_subtask 标记完成;必要时 revise_current_plan 调整;
3. 若 analyze_competitor_course 返回解析失败,勿停止:改用 google_search、extract_text_from_pdf 等替代方案;
4. 全部完成后 finish_plan 收尾,并给用户一段中文调研摘要。
""")
.model(model)
.toolkit(toolkit)
.planNotebook(planNotebook)
.enablePlan()
.memory(new InMemoryMemory())
.maxIters(48)
.build();
Msg userMsg = Msg.builder()
.textContent("请帮我完成一门新的 Python 入门课程的前期调研,竞品是 some-site.com 的课程。")
.build();
// 首轮:部分模型仍会礼貌性询问「是否开始」;同一 Agent + InMemoryMemory 可继续多轮对话
Msg response1 = Objects.requireNonNull(agent.call(userMsg).block(), "Agent 首轮返回为空");
String text1 = Objects.requireNonNullElse(response1.getTextContent(), "");
System.out.println("=== 首轮答复 ===");
System.out.println(text1);
// 若模型仍停在「请确认」,第二轮用明确授权推进(产品里可换成 UI「开始」按钮发送同义句)
Msg confirm = Msg.builder()
.textContent("是。已授权,请立即执行完整计划:依次 create_plan、调用工具完成各子任务;"
+ "不要再次询问确认;竞品分析失败后必须 google_search / extract_text_from_pdf 绕行;"
+ "最后 finish_plan 并输出中文调研摘要。")
.build();
Msg response2 = Objects.requireNonNull(agent.call(confirm).block(), "Agent 第二轮返回为空");
String text = Objects.requireNonNullElse(response2.getTextContent(), "");
System.out.println("=== Plan 快照(钩子)===");
PLAN_SNAPSHOT_LINES.forEach(System.out::println);
System.out.println("=== 最终答复(第二轮)===");
System.out.println(text);
Assertions.assertFalse(text.isBlank(), "模型未返回可见文本");
}
/** 将 Plan 与子任务状态压成一段字符串,便于 println 或断言解析(此处仅打印)。 */
private static String renderPlanSnapshot(Plan plan) {
StringBuilder sb = new StringBuilder();
sb.append("plan=").append(plan.getName())
.append(" state=").append(plan.getState());
List<SubTask> tasks = plan.getSubtasks() == null ? List.of() : plan.getSubtasks();
for (int i = 0; i < tasks.size(); i++) {
SubTask st = tasks.get(i);
sb.append("\n ").append(i + 1).append(". ")
.append(st.getName())
.append(" [").append(st.getState()).append("]");
}
return sb.toString();
}
/** 课件中的四个工具:返回字符串由模型读入并决定下一步(含失败后的绕行)。 */
static final class CourseResearchTools {
@Tool(name = "analyze_competitor_course", description = "分析竞品课程页面的大纲(可能因网站改版失败)")
public String analyzeCompetitorCourse(
@ToolParam(name = "url", description = "竞品课程页面 URL 或域名") String url) {
// 刻意模拟「页面改版」导致固定流会卡死的错误返回
return "❌ 错误:因 " + url + " 网站布局更新,无法解析课程大纲。";
}
@Tool(name = "search_industry_demand", description = "查询与主题相关的行业技能需求摘要")
public String searchIndustryDemand(
@ToolParam(name = "topic", description = "行业或技能主题") String topic) {
return "✅ 报告:关于「" + topic + "」的行业需求分析已完成。";
}
@Tool(name = "google_search", description = "网页搜索,用于竞品工具失败时的替代信息搜集")
public String googleSearch(@ToolParam(name = "query", description = "搜索关键词") String query) {
if (query != null && query.toLowerCase().contains("syllabus")) {
return "搜索结果:找到了 'Python入门课程' 的大纲 PDF,地址 a.com/syllabus.pdf";
}
return "未找到相关信息";
}
@Tool(name = "extract_text_from_pdf", description = "从 PDF 链接提取大纲文本(示例实现)")
public String extractTextFromPdf(@ToolParam(name = "url", description = "PDF 地址") String url) {
return "✅ 已从 " + url + " 提取大纲文本:1. 变量与数据类型… 2. 控制流…(示例)";
}
}
}
运行:
export DASHSCOPE_API_KEY=sk-...
cd baoma_ai_video_platform_java
mvn -pl module-ai test -Dtest=CourseResearcherPlanNotebookAgentScopeTest
说明:即使 PlanNotebook.needUserConfirm(false),部分模型仍可能口头问「是否开始」。单测采用 同一 ReActAgent 连续两轮 agent.call(...):第二轮发「是。已授权…」等价你在聊天框里回复「是」,对话历史在 InMemoryMemory 里延续,从而推进 create_plan 与后续工具调用。
角色一览与代码映射(Plan 生态 → 如何落成代码)
上一节单测把「自主 / 可调整规划」拆成了多个框架角色。下表说明:设计里每个角色是谁、对应哪类 API、在代码里干什么;实现时按表后的顺序组装即可与 CourseResearcherPlanNotebookAgentScopeTest 对齐。
角色一览
| 设计角色 | 典型框架类 / 能力 | 在代码中的职责 |
|---|---|---|
| 大脑(策略) |
DashScopeChatModel 等 Model
|
决定下一步:调用哪个工具、生成什么自然语言;本文用 AgentScopeDashScopeModels.buildDashScopeChatModel(apiKey, modelName) 统一构造。 |
| 执行循环 |
ReActAgent + enablePlan()
|
多轮「推理 → tool_calls → 观察」;enablePlan() 表示启用与 PlanNotebook 协同的计划模式。 |
| 计划状态核心 | PlanNotebook |
维护当前 Plan、子任务列表与状态;addChangeHook 在计划变更时回调,便于日志 / 审计 / UI。 |
| 计划持久化 |
PlanStorage(实现如 InMemoryPlanStorage) |
计划存哪里:内存单测用 InMemoryPlanStorage;生产可换可持久化存储(以 AgentScope 提供的实现为准)。 |
| 计划领域对象 |
Plan、SubTask、SubTaskState
|
钩子入参里的 Plan 即这些数据;可读 name、state、subtasks 做观测或断言。 |
| 计划 → 提示(可选) |
PlanToHint(PlanNotebook.builder().planToHint(...)) |
把当前计划摘要注入模型上下文,减轻「模型忘记进度」;单测可不配,按业务再开。 |
| 对话记忆 |
Memory(如 InMemoryMemory) |
同一 ReActAgent 多轮 call 时延续上下文(例如第二轮「是,已授权…」)。 |
| 业务工具 |
Toolkit.registerTool(你的 Bean) + @Tool
|
真实业务:检索、解析、搜索、读 PDF 等;返回文本供模型继续推理。 |
| 元工具(改计划的手) | Toolkit.registerMetaTool() |
注册框架内置的 create_plan、finish_subtask、revise_current_plan、finish_plan 等;由框架实现,通过 tool 调用读写 PlanNotebook,不要自造同名业务工具抢职责。 |
PlanNotebook.builder() 链式调用释义(与 CourseResearcherPlanNotebookAgentScopeTest 内注释一致)
| 调用 | 含义 |
|---|---|
PlanNotebook.builder() |
开始链式配置「计划本」;后续元工具读写的状态都挂在此实例上。 |
.storage(new InMemoryPlanStorage()) |
计划存进程内内存,单测/演示用完即丢;生产可换可持久化的 PlanStorage 实现。 |
.maxSubtasks(24) |
单份计划允许的子任务个数上限,防止模型无限拆步撑爆上下文或存储。 |
.needUserConfirm(false) |
框架侧尽量不要求用户对计划变更节点做显式确认;模型仍可能口头问「是否开始」,与该项正交。 |
.build() |
得到可用的 PlanNotebook,再传入 ReActAgent.builder().planNotebook(...).enablePlan()。 |
从设计到代码的实现顺序(最小闭环)
-
构造模型:
var model = AgentScopeDashScopeModels.buildDashScopeChatModel(apiKey, modelName); -
注册业务工具:
Toolkit toolkit = new Toolkit(); toolkit.registerTool(new CourseResearchTools()); -
注册元工具:
toolkit.registerMetaTool();—— 把create_plan等挂进工具表,否则模型往往无法正规「建计划 / 改计划」。 -
构造计划本:
PlanNotebook.builder().storage(new InMemoryPlanStorage()).maxSubtasks(24).needUserConfirm(false).build(); -
(可选)注册变更钩子:
planNotebook.addChangeHook("capture", (notebook, plan) -> { ... }); -
组装 Agent:
ReActAgent.builder().model(model).toolkit(toolkit).planNotebook(planNotebook).enablePlan().memory(new InMemoryMemory()).maxIters(48).sysPrompt("...").build(); -
驱动对话:
agent.call(userMsg).block();若模型仍口头索要确认,同一 agent 实例再agent.call(confirmMsg).block()(产品里对应「开始执行」按钮)。
落地原则:用户 Goal 写在 sysPrompt / user 消息里;子任务拆分与修订交给「元工具 + PlanNotebook」;失败绕行同时依赖业务工具的错误返回文案与系统提示中的分支约定。
扩展阅读(生产级):LangGraph、Spring AI Alibaba Graph、AgentScope 等均支持「规划–执行」循环;云上可配合 PAI / DashScope 做模型部署与观测。
通过这个示例,你可以看到:
- 自主创建计划:Agent 使用 create_plan 工具自动规划调研任务。
- 灵活执行:遇到竞品分析工具失效时,Agent 自动调整策略,转而使用 google_search。
- 进度追踪:使用 finish_subtask 标记完成的任务。
- 完整闭环:从规划创建到任务完成的全流程。
这正是 PlanNotebook 为 Agent 带来的核心能力:将其从"流程执行者"提升为"问题解决者"。
扩展阅读:生产级框架
像 AgentScope 和 LangChain 这样的开源框架,都提供了实现这种“规划-执行”循环的机制。它们允许你定义一系列工具,然后让大模型作为规划器 (Planner) 来决定在每一步应该调用哪个工具,并将工具返回的结果作为后续思考的输入,从而实现复杂的任务拆解和执行。在阿里云机器学习平台 PAI 上,你可以方便地部署和管理这些框架所需的大模型服务,为 Agent 提供强大的“大脑”。
3.4 执行 Agent 生成的规划(JSON / Code as Action)
那么,如何让大模型生成一份机器可以理解和执行的“计划”呢?
最简单的方式,是让它生成自然语言的步骤列表。但这样做,下游的执行程序很难精确解析。你之前在让 Agent 调用工具时学过,可以使用结构化的 JSON 格式 输出工具调用参数。这里,你也可以把“执行计划”看作调用工具。每一个步骤都是一个定义清晰的对象,包含要调用的工具名和对应的参数。
JSON 示例(课件):
{
"plan": [
{
"step": 1,
"thought": "我首先需要分析行业需求,这是课程定位的关键。",
"tool_name": "search_industry_demand",
"tool_params": {"topic": "Python 基础"}
},
{
"step": 2,
"thought": "接下来,我尝试分析竞品课程的大纲。",
"tool_name": "analyze_competitor_course",
"tool_params": {"url": "some-site.com/python-course"}
}
]
}
这是一种有效的方法,但它的表达能力有限。如果计划中需要包含“如果竞品分析失败,则改用谷歌搜索”这样的条件逻辑,简单的 JSON 列表就难以胜任了。
为了表达更复杂的逻辑,你可以让大模型直接生成代码 (Code as Action) 来表达其计划,再通过调用“代码解释器”这个工具来执行代码。
plaintext
# Plan generated by LLM
def execute_research_plan():
# Step 1: Analyze industry demand
demand_result = search_industry_demand(topic="Python 基础")
print(demand_result)
# Step 2: Analyze competitor course
competitor_result = analyze_competitor_course(url="some-site.com/python-course")
# Step 3: Handle analysis failure
if not competitor_result.success and "无法解析" in competitor_result.message:
print("竞品分析工具失效,正在寻找备选方案...")
search_results = google_search(query="some-site.com python course syllabus")
# Assume search_results gives a PDF link
pdf_url = extract_pdf_link(search_results)
if pdf_url:
syllabus_text = extract_text_from_pdf(url=pdf_url)
print(syllabus_text)
else:
print(competitor_result)
execute_research_plan()
通过生成代码,大模型可以利用编程语言内置的丰富能力(如变量、条件判断、循环)和强大的第三方库(如 Pandas)来制定和执行极其复杂的计划。这使得 Agent 不仅能应对简单的线性流程,还能处理包含逻辑判断和数据处理的复杂场景。
3.5 进阶:让 Agent 创建新工具(动态工具)
上面的方法可以让大模型通过生成代码来制定计划的强大方法。这种方式赋予了 Agent 运用变量、条件判断和循环等复杂逻辑的能力。
局限性:
Agent 仍受限于初始工具集;
如果它在执行计划时,发现需要一个你并未提供的新工具,比如一个用于计算不同技术关键词在招聘网站上出现频率的函数,它该怎么办?
高阶做法是提供 元工具(如代码解释器),在受控条件下注册新能力。专业名词叫做动态创造工具 (Dynamic Tool Creation)。
实现条件要满足:
1.需要提供具体业务工具(如 analyze_competitor_course)。
- 还需要再为Agent 提供一个核心元工具:代码执行环境 (Code Interpreter)
当 Agent 识别到现有工具无法满足需求时,它的规划会包含一系列特殊的步骤:
- 决策:大模型分析任务,识别出需要一个当前不存在的新工具。
- 生成代码:在它的计划中,它会编写一段代码来定义、测试并封装一个新的工具函数。
- 调用新工具:在新工具于代码执行环境中被成功创建后,Agent 可以在后续的计划步骤中直接调用它,就好像这个工具一开始就存在一样。
-
扩展工具库:这个新生成的工具可以被加入到本次任务的临时工具库中,供后续步骤复用。
image.png
Java:ToolMakerDynamicToolAgentScopeTest.java 全文(白名单注册,禁止任意 exec)
package com.baoma.ai.debug;
// 动态工具演示:同一 ReActAgent 多轮对话;第二轮经受限 code_exec 向 Toolkit 注册 factorial,第三轮调用新工具。
import com.baoma.ai.debug.support.AgentScopeDashScopeModels;
import io.agentscope.core.ReActAgent;
import io.agentscope.core.memory.InMemoryMemory;
import io.agentscope.core.message.Msg;
import io.agentscope.core.tool.Tool;
import io.agentscope.core.tool.ToolParam;
import io.agentscope.core.tool.Toolkit;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Assumptions;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import java.util.Objects;
import java.util.concurrent.atomic.AtomicBoolean;
/** 三场景串联:add → code_exec(白名单)→ factorial;AtomicBoolean 用于断言「已注册」。 */
class ToolMakerDynamicToolAgentScopeTest {
@Test
@DisplayName("AgentScope:add → code_exec 注册 factorial → 调用 factorial(多轮同一 Agent)")
void toolMakerRegistersFactorialAcrossTurns() {
String apiKey = AgentScopeDashScopeModels.firstNonBlank(
System.getenv("AGENTSCOPE_DEBUG_API_KEY"),
System.getenv("REASONING_DEBUG_API_KEY"),
System.getenv("PROMPT_DEBUG_API_KEY"),
System.getenv("DASHSCOPE_API_KEY")
);
Assumptions.assumeTrue(apiKey != null && !apiKey.isBlank(),
"请配置 DASHSCOPE_API_KEY(或 AGENTSCOPE_DEBUG_API_KEY 等)后重跑");
String modelName = AgentScopeDashScopeModels.firstNonBlank(
System.getenv("AGENTSCOPE_TOOLMAKER_MODEL"),
"qwen-plus"
);
Toolkit toolkit = new Toolkit();
AtomicBoolean factorialRegistered = new AtomicBoolean(false);
toolkit.registerTool(new AddTools());
// CodeSandbox 持有 toolkit 引用,便于在「通过白名单」后 registerTool
toolkit.registerTool(new CodeSandbox(toolkit, factorialRegistered));
var model = AgentScopeDashScopeModels.buildDashScopeChatModel(apiKey, modelName);
ReActAgent agent = ReActAgent.builder()
.name("ToolMaker")
.sysPrompt("""
你是 ToolMaker。可用工具:add、code_exec。
- 两数相加请调用 add。
- 若用户要创建阶乘工具:调用 code_exec,且 code 参数必须是一段包含英文关键字 def 与 factorial 的 Python 风格代码(教学演示白名单)。
- 注册成功后,若用户要求计算阶乘,请调用 factorial 工具(参数 n 为整数)。
""")
.model(model)
.toolkit(toolkit)
.memory(new InMemoryMemory())
.maxIters(32)
.build();
Msg r1 = Objects.requireNonNull(agent.call(Msg.builder().textContent("计算 30 + 45").build()).block());
String t1 = Objects.requireNonNullElse(r1.getTextContent(), "");
System.out.println("=== 场景1 ===\n" + t1);
Assertions.assertTrue(t1.contains("75") || (t1.contains("30") && t1.contains("45")),
"期望加法结果,实际: " + t1);
Msg r2 = Objects.requireNonNull(agent.call(Msg.builder()
.textContent("请通过 code_exec 注册 factorial:提交包含 def factorial 的 Python 代码片段。")
.build()).block());
String t2 = Objects.requireNonNullElse(r2.getTextContent(), "");
System.out.println("=== 场景2 ===\n" + t2);
Assertions.assertTrue(factorialRegistered.get(), "code_exec 应触发 factorial 注册");
Msg r3 = Objects.requireNonNull(agent.call(Msg.builder()
.textContent("用 factorial 计算 5 的阶乘。")
.build()).block());
String t3 = Objects.requireNonNullElse(r3.getTextContent(), "");
System.out.println("=== 场景3 ===\n" + t3);
Assertions.assertTrue(t3.contains("120"), "5! = 120,实际: " + t3);
}
static final class AddTools {
@Tool(name = "add", description = "计算两数之和")
public String add(
@ToolParam(name = "a", description = "加数 a") double a,
@ToolParam(name = "b", description = "加数 b") double b) {
double s = a + b;
if (Math.rint(s) == s) {
return "%s + %s = %d".formatted(strip(a), strip(b), (long) s);
}
return "%s + %s = %s".formatted(strip(a), strip(b), Double.toString(s));
}
private static String strip(double x) {
return Math.rint(x) == x ? String.valueOf((long) x) : Double.toString(x);
}
}
static final class FactorialTools {
@Tool(name = "factorial", description = "计算非负整数 n 的阶乘(演示,n<=20)")
public String factorial(@ToolParam(name = "n", description = "整数 n") int n) {
if (n < 0 || n > 20) {
return "错误: n 应在 0..20";
}
long r = 1;
for (int i = 2; i <= n; i++) {
r *= i;
}
return String.valueOf(r);
}
}
static final class CodeSandbox {
private final Toolkit toolkit;
private final AtomicBoolean factorialRegistered;
CodeSandbox(Toolkit toolkit, AtomicBoolean factorialRegistered) {
this.toolkit = toolkit;
this.factorialRegistered = factorialRegistered;
}
@Tool(
name = "code_exec",
description = "受限代码沙箱(演示):仅当 code 含 def 与 factorial 时向工具箱注册 factorial;禁止任意执行未审核代码。"
)
public String codeExec(@ToolParam(name = "code", description = "待检测的代码文本") String code) {
if (code == null || code.isBlank()) {
return "❌ 代码为空";
}
String lower = code.toLowerCase();
if (lower.contains("factorial") && (lower.contains("def ") || lower.contains("async def"))) {
if (toolkit.getToolNames().contains("factorial")) {
factorialRegistered.set(true);
return "ℹ️ factorial 已存在";
}
toolkit.registerTool(new FactorialTools());
factorialRegistered.set(true);
return "✅ 已通过白名单并注册 factorial";
}
return "❌ 未命中演示白名单(需同时包含 factorial 与 def/async def)";
}
}
}
通过提供代码解释器,你将 Agent 从一个单纯的工具使用者,提升为了一个工具创造者。它的能力边界不再被你预先定义的工具集所束缚,从而具备了真正的创造性和问题解决的适应性。
3.6 何时选择自主规划?(探索–固化)
前面已经介绍了“固定工作流”和“自主规划”两种模式,你可能会问:我是不是应该在所有场景下都使用更智能的自主规划,彻底放弃固定工作流?
这样想是不对的。自主性更高的模式不是银弹,自主性更低的模式也有广泛的应用场景。在生产实践中,一种非常有效的最佳实践是采用“探索-固化”混合模式。
这种模式将任务处理分为两个阶段:
探索阶段:对于新出现的、流程不明确的任务(例如,你需要调研一个全新的、之前从未接触过的小众技术领域),你无法预先定义一个完美的流程。这时,就应该派出自主规划 Agent。它的任务是探索解决问题的不同路径,调用它认为合适的工具,即便过程中会犯错或走到死胡同,最终的目标是找到一条能稳定解决问题的方案。
固化阶段:当自主规划 Agent 经过多次探索,验证并总结出一条稳定、高效的解决方案路径后(例如,它发现“先用A工具从特定网站爬取信息,再用B工具进行数据清洗,最后用C工具生成总结报告”的流程成功率最高),你就可以将这条被验证过的路径抽象并固化下来,封装成一个可靠的“固定工作流”,用于后续大规模、重复性的生产调用。
这样,你就建立了一个持续优化的正向循环。
3.7 案例分析:网页操作 Agent(Browser Use 等)
为了让你更具体地理解这种“感知-规划-行动”循环在实际产品中的应用,让我们来看一个高自主性网页操作 Agent 的案例,例如开源项目 Browser Use。
传统的网页自动化(RPA)工具需要为每个网站、每个任务编写固定的操作脚本。一旦网站界面稍有改动,脚本就会失效,维护成本极高。
一个具备规划能力的 Agent 则可以从根本上解决这个问题。它不依赖固定的脚本,而是像人一样理解用户的目标,并感知当前的网页状态,动态地规划出下一步操作。
执行流程拆解: 当用户给出指令 “在亚马逊上搜索关于 AI 的书籍” 时:
- 理解与初步规划:LLM 将模糊的目标分解为一系列高阶步骤:
“1. 打开亚马逊网站;2. 找到搜索框;3. 输入'AI书籍';4. 点击搜索;5. 分析结果。” - 行动与感知:Agent 执行第一步(打开网站)。然后它“感知”新页面——这不仅是看 HTML 代码,还可能包括分析截图的视觉布局,来理解页面上有哪些元素。
- 决策与再规划:基于感知到的信息,它决策下一步行动:找到那个看起来最像“搜索框”的输入区域。如果页面上有多个输入框,它会根据位置、标签等信息进行推理判断。
- 循环执行:它持续这个“感知-规划-行动”的循环ReAct,直到完成所有步骤,并返回搜索到的书籍列表。
-
异常处理:如果在任何一步遇到意外,比如点击搜索后弹出一个验证码,它不会卡住。它会感知到这个新情况,并将“处理验证码”作为一个新的障碍插入到当前计划中,尝试解决它或向用户求助。
image.png
这个案例完整地展示了规划型 Agent 的核心优势:它不再是脚本执行器,而是通过持续的“感知-规划-行动”循环,实现了对动态、未知网页环境的真正自适应操作。
3.8 总结
| 主题 | 要点 |
|---|---|
| 固定工作流 | 工具/环境一变易「卡死」;无限加分支不可维护 |
| 规划 | 围绕 Goal 动态生成/修订 Plan |
| PlanNotebook(Java) |
PlanNotebook + enablePlan() + 业务 Toolkit + registerMetaTool()
|
| 计划落地 | JSON + 执行器循环;或 ReAct 内多轮工具;或受控 Code-as-Action |
| 动态工具 | 元工具 + 安全策略;上文 ToolMakerDynamicToolAgentScopeTest 已全文内嵌 |
| 工程权衡 | 探索用自主规划,规模化用固化流程;结合 HITL |

