侦察兵Research Scout
给模型装上工具和结构化输出Give the model tools and typed output
任务简报Brief
研究员让 Agent 查本地资料库、把结论写成简报并返回严格 JSON。下游系统只接受 ResearchBrief,不接受“看起来差不多”的文本。
A researcher asks an agent to search a local corpus, write a brief and return strict JSON. The downstream system accepts ResearchBrief, not free-form text that looks close enough.
只会聊天的模型无法改变世界;能调用工具的模型才可以。这一关把“工具”与“结构化输出”放在一起练:模型先决定调用哪个工具、传什么参数,再读取工具结果,最后用 generate_response 把答案压进 schema。你会亲眼看到 schema 如何同时约束模型和下游代码。
A model that only chats cannot change the world; one that calls tools can. This mission pairs tools with structured output: the model chooses a tool and arguments, reads the result, then uses generate_response to squeeze the answer into a schema. You will see how one schema constrains both the model and downstream code.
学习目标Learning goals
用
FunctionTool/@Tool暴露search_notes与save_brief。Expose
search_notesandsave_briefwithFunctionTool/@Tool.让模型在一次 ReAct 循环里连续调用两个工具。
Let the model call two tools in one ReAct loop.
用 Pydantic / record 接收结构化结果,并验证字段。
Receive and validate structured output with Pydantic / a record.
确保工具真的产生了副作用文件,而不是只返回一句话。
Ensure the tool actually writes its artifact instead of only returning text.
核心概念Core concepts
Toolkit
工具注册表;负责把函数签名变成模型可读的 JSON Schema,再路由执行。
The tool registry that turns signatures into model-readable JSON Schema and routes execution.
FunctionTool / @Tool
Python 从函数签名与 docstring 生成 schema;Java 从注解与方法参数生成。
Python derives schema from signature and docstring; Java derives it from annotations and parameters.
ToolUseBlock / ToolResultBlock
模型发出的调用意图与工具返回结果;它们是消息内容的一部分,而不是隐藏状态。
The model's call intent and the tool's result; both are message content, not hidden state.
agentscope.message.ToolUseBlockio.agentscope.core.message.ToolUseBlockStructured output
强制最终答案符合 schema;Python 用 structured_schema,Java 用 call(..., Type.class)。
Forces the final answer to match a schema; Python uses structured_schema, Java uses call(..., Type.class).
双轨实现Build it twice
先完整跑通一条轨道,再切换重来。两条轨道写出同一种产物,方便直接对照行为。Run one track end to end, then switch and repeat. Both tracks write the same artifact so you can compare behavior directly.
工具函数用 Google 风格 docstring 描述参数与返回值;参数类型注解会被转换成 JSON Schema。不需要手写 schema。
Describe tool parameters and returns in a Google-style docstring; type hints become JSON Schema. No hand-written schema required.
声明工具并注册Declare and register tools
只读工具建议标记 is_read_only=True,权限引擎和 UI 都能据此做更安全的决策。
Mark read-only tools with is_read_only=True; the permission engine and UI can make safer decisions.
from agentscope.tool import FunctionTool, Toolkit
from common.tools import save_brief, search_notes
toolkit = Toolkit(
tools=[
FunctionTool(search_notes, permission=LAB_ALLOW, is_read_only=True),
FunctionTool(save_brief, permission=LAB_ALLOW),
],
)
定义结构化结果Define the structured result
Pydantic 模型既约束模型输出,也约束下游代码。Field(ge=0, le=1) 这种约束会在校验时生效。
The Pydantic model constrains both model output and downstream code. Constraints such as Field(ge=0, le=1) are enforced during validation.
from pydantic import BaseModel, Field
class ResearchBrief(BaseModel):
title: str = Field(description="Short title of the brief")
findings: list[str] = Field(description="Concrete findings")
confidence: float = Field(ge=0, le=1)
reply = await agent.reply(
UserMsg("user", "Research ReAct guardrails and save a brief."),
structured_schema=ResearchBrief,
)
print(reply.structured_output) # dict, already validated
检查副作用Check the side effect
check 不只看回复文本,还读取 lab/work/m1/brief.json,确认工具真的落盘了三段 findings。
The check does not stop at reply text; it reads lab/work/m1/brief.json and confirms three findings were persisted.
python lab.py check m1
cat work/m1/brief.json
# 期望看到
# PASS CODE: AS-M1-K2VD
python lab.py check m1
search_notes 是本地关键词检索,不依赖网络;save_brief 写入 lab/work/m1/,两个轨道共用同一个产物路径。
search_notes is local keyword search with no network; save_brief writes into lab/work/m1/, shared by both tracks.
lab/missions/m1_researcher/mission.py判题证明了什么What the check proves
- Toolkit 暴露了 search_notes 与 save_brief 的 schemaToolkit exposed schemas for search_notes and save_brief
- Agent 在一次 ReAct 循环里执行了两个工具The agent executed two tools inside one ReAct loop
- 结构化输出通过了 ResearchBrief 校验Structured output passed the ResearchBrief schema
- brief.json 已由工具真实写入brief.json was really written by the tool
Java 用 @Tool / @ToolParam 注解把方法注册进 Toolkit。参数类型必须是 Jackson 可序列化的类型;List<String>、Double 这类都没问题。
Java registers methods through @Tool / @ToolParam. Parameters must be Jackson-serializable; List<String> and Double are fine.
用注解声明工具Declare tools with annotations
required=false 对应可选参数;schema 会生成默认值信息。readOnly=true 是给权限引擎和 UI 的语义标记。
required=false marks optional parameters; readOnly=true is a semantic hint for the permission engine and UI.
public final class BriefTools {
@Tool(name = "search_notes", readOnly = true,
description = "Search the local research corpus.")
public String searchNotes(
@ToolParam(name = "query") String query,
@ToolParam(name = "limit", required = false) Integer limit) { ... }
@Tool(name = "save_brief", description = "Save a research brief.")
public String saveBrief(
@ToolParam(name = "title") String title,
@ToolParam(name = "findings") List<String> findings,
@ToolParam(name = "confidence", required = false) Double confidence) { ... }
}
注册并请求结构化结果Register and request typed output
call(..., ResearchBrief.class, context) 会让 ReActAgent 自动加上 generate_response 工具;最终结果在 Msg 元数据的 _structured_output 里。
call(..., ResearchBrief.class, context) makes ReActAgent add a generate_response tool; the result lands in the _structured_output metadata of the Msg.
Toolkit toolkit = new Toolkit();
toolkit.registerTool(new BriefTools());
ReActAgent agent = ReActAgent.builder()
.name("scout").sysPrompt("Search, save a brief, return typed output.")
.model(model).toolkit(toolkit).maxIters(6)
.permissionContext(PermissionContextState.builder()
.mode(PermissionMode.BYPASS).build())
.build();
Msg reply = agent.call(List.of(LabSupport.user("Research ReAct guardrails.")),
ResearchBrief.class, context).block();
编译并判题Compile and check
检查会验证 schema 名称、三条 findings、confidence 范围和 brief.json。如果工具结果报错,先看回复里是不是出现了 “Tool not found”。
The check validates schema names, three findings, the confidence range and brief.json. If a tool fails, look for “Tool not found” in the reply.
cd lab/java
mvn -q -o compile exec:java -Dexec.args="check m1"
# 期望看到
# PASS CODE: AS-M1-K2VD
cd lab/java && mvn -q -o compile exec:java -Dexec.args="check m1"
Java 的 ToolExecutor 从 ToolUseBlock.getContent() 读取并校验参数;自写 FakeChatModel 脚本时要保证 input 与 content 一致。
Java's ToolExecutor validates from ToolUseBlock.getContent(); when scripting a FakeChatModel, keep input and content consistent.
lab/java/src/main/java/lab/m1/M1Researcher.java判题证明了什么What the check proves
- Toolkit 暴露了注解工具 schemaToolkit exposed annotated tool schemas
- Agent 在 ReAct 循环里执行了搜索与保存The agent searched and saved inside the ReAct loop
- 结构化输出绑定到 ResearchBriefStructured output bound to ResearchBrief
- brief.json 已持久化brief.json persisted
Python ↔ Java 差异Python ↔ Java differences
Python 从 docstring/类型注解推导 schema;Java 用
@Tool/@ToolParam显式描述。两边最终都变成模型可读的 JSON Schema。Python derives schema from docstrings/type hints; Java uses
@Tool/@ToolParam. Both end up as model-readable JSON Schema.Python 返回
reply.structured_output字典;Java 从Msg.getMetadata().get("_structured_output")取 Map,再转换成 record。Python returns a
structured_outputdict; Java reads_structured_outputfromMsgmetadata and converts it to a record.Java 的结构化输出依赖内置工具
generate_response;调试时如果模型没调用它,回复会是普通文本。Java structured output relies on the built-in
generate_responsetool; if the model skips it, you get plain text.
常见坑Pitfalls
工具参数类型不是 JSON 可序列化的:例如自定义对象没有无参构造/序列化器。保持参数为字符串、数字、布尔、列表和简单 Map。
Non-JSON-serializable tool parameters break schema generation. Keep parameters to strings, numbers, booleans, lists and simple maps.
只检查文本回复:工具可能返回 Error 但模型仍然礼貌地回答。必须读取 ToolResultBlock 的状态与内容。
Checking only reply text: a tool may return an Error while the model still answers politely. Inspect the ToolResultBlock state and content.
把
confidence当成模型自觉:它只是字段。生产环境要用外部校验、置信度阈值或人工复核。Treating
confidenceas self-awareness: it is only a field. Production needs external validation, thresholds or human review.
领取通行码Claim your pass code
先运行当前轨道的 check。CLI 会打印 PASS CODE、XP 与徽章。粘贴到这里;同一关在每条轨道可各领取一次。Run the check for the current track. The CLI prints PASS CODE plus the exact XP and badge. Paste it here; the same mission can be claimed once on each track.
费曼复述Feynman recall
合上教程,用自己的话讲清机制。关键点覆盖 65%、篇幅 20%、结构 15%;60 分通过并一次性获得 20 XP。Explain the mechanism without looking. Coverage 65%, length 20%, structure 15%; 60 passes and earns +20 XP once.
为什么需要 Toolkit 和结构化输出?模型调用工具的一轮里,消息是怎样流动的?
Why do we need Toolkit and structured output? How do messages flow during one tool-calling turn?
查看参考答案Show the model answer
Toolkit 把函数签名变成模型能理解的 schema,让模型知道有哪些工具、参数怎么填;工具调用与结果作为消息块进入上下文,模型据此继续推理。结构化输出再把最终答案约束成固定字段,避免下游解析自由文本。一次回合里,消息依次是:系统提示 → 用户消息 → 工具调用 → 工具结果 → 最终结构化结果。
Toolkit turns signatures into schemas the model can understand, so it knows which tools exist and how to fill their arguments. Tool calls and results enter the context as message blocks, and the model continues reasoning from them. Structured output then constrains the final answer to fixed fields, so downstream code does not parse free text. One turn flows as system prompt → user message → tool call → tool result → final structured result.
间隔复习Spaced review
首次领取后会在明天安排复习。按真实感受评分,下一次间隔会自适应。After the first claim a review is scheduled for tomorrow. Rate how it felt; the next interval adapts.