从0开始搭建个人最简harness项目
本项目作为一个教学示例,提供了最精简却功能完整的 Agent 实现。通过跟随本指南逐步操作,您将深入理解 harness 框架的核心概念及对应代码实现。

目标
这是一个教学项目,包含了 Agent 最精简但完整的实现。跟随本指南逐步操作,您将透彻理解 harness 的关键概念与代码实现细节。
项目包含
- Agent Loop:Agent 的主循环,反复执行“调用模型 → 决策 → 调用工具 → 观察结果”这一流程,直到满足终止条件为止。
- Tool:Agent 可调用的外部能力(例如执行命令、读写文件),每个工具都包含名称、参数 schema 以及执行逻辑。
- Tool Use:模型选择并调用工具的机制,包含参数校验与错误处理环节。
- Context:发送给 LLM 的消息序列(包括 system/user/assistant/tool 等角色)。
- Context 压缩:当上下文过长时,通过摘要或截断等手段缩短长度,避免超出模型窗口限制。
- Memory 分层:将记忆按生命周期与用途分层管理,例如分为短期对话、工作记忆、长期记忆等层次。
- Eval:使用一组任务用例自动化衡量 Agent 的表现,例如任务通过率等指标。
- Trace:记录一次运行中每一步的输入、输出、耗时等信息,用于后续回放与问题排查。
- Docker 沙箱:在受限的 Docker 容器中执行工具(尤其是命令执行),从而隔离对宿主机的影响。
- 支持Skill:基于 SKILL.md 的可复用技能,采用渐进式披露策略(先注入名称与描述,按需再加载正文与脚本),减少上下文占用。
- 支持MCP:内置基于 stdio + JSON-RPC 2.0 的最小 MCP 客户端,将外部 server 的工具并入统一注册表,与本地工具在同一条调用链路上使用。
前置工作
- 申请任意大模型 API 并确保账户内有充足余额。
- 本项目使用 DeepSeek,平台地址为 platform.deepseek.com。
设计细节
Agent Loop
本项目采用了两层循环架构:外层为 Session 循环,负责处理一次 CLI 会话中的连续对话;内层为 Agent Loop,负责执行单轮用户输入的 step 迭代。Agent 与 LLM 会在单个 turn 内进行多轮沟通,期间可能包含多次工具调用,直到 LLM 判定当前任务已完成,或者循环次数达到 max_step 上限。
CLI 打开 → 新建 session (session_id),加载长期记忆
│
▼
┌═════════════════════ Session 循环 (REPL) ═══════════════════════┐
│ 读取用户输入: │
│ ├─ 退出指令 (/exit, quit, Ctrl-D) → 结束 session │
│ └─ 否则当作一轮输入 (turn) ↓ │
│ │
│ ┌────────────────────── Agent Loop ─────────────────────────┐ │
│ │ (turn 级:终止判断 / 容错 / 重试;编排各模块) │ │
│ │ step N: │ │
│ │ 1) 组装上下文 ← Memory(分层) + 历史消息 │ │
│ │ 2) Context 压缩(若超阈值) │ │
│ │ 3) 调用 LLM (DeepSeek, OpenAI 兼容, 传 tools schema) │ │
│ │ 4) 解析响应: │ │
│ │ ├─ 有 tool_calls → 校验参数 → (副作用类)经沙箱执行 │ │
│ │ │ → observation → 回灌上下文 │ │
│ │ └─ 无 tool_call / finish → 命中本轮终止 → 输出答复 │ │
│ │ 5) 追加短期记忆(消息) + 记录 Trace(本步 span) │ │
│ │ 6) 回到 step N+1,直到本轮终止 / max_steps │ │
│ └───────────────────────────────────────────────────────────┘ │
│ 输出答复 → 回到 REPL 等下一轮输入 │
│ (短期 & 工作记忆在 session 内跨轮共享) │
└═════════════════════════════════════════════════════════════════┘
│ │
▼ ▼
会话结束 trace 文件 (全链路)(Eval 在最外层:批量喂输入 → 跑内层 Agent Loop → 按用例断言判分)
对于单次 session(即用户打开命令行工具),系统会反复进行用户提问-进入 turn 的循环操作。
def repl(self) -> None: # pragma: no cover - 交互入口
"""一次 CLI 打开 = 一个 session:反复读取输入,仅显式退出才结束。"""
print(f"harness101 会话开始(session={self.session_id})。输入 /exit 或 quit 退出。")
try:
while True:
try:
user_input = input("n你> ").strip()
except EOFError:
print()
break
if user_input in ("/exit", "quit", "exit"):
break
if not user_input:
continue
result = self.run_turn(user_input)
print(f"n助手> {result.answer if result.answer is not None else '(本轮未产出答复)'}")
if result.terminated_by not in ("final_answer", "finish"):
print(f"[本轮 terminated_by={result.terminated_by}, steps={result.steps}]")
finally:
self.close()
print("会话结束。")
对于单个 turn,系统采用 ReAct 模式。Agent 与大模型之间反复沟通:大模型提议调用工具,Agent 执行并返回结果给大模型,大模型再给出下一步行动,直至任务完成。
def run_turn(self, user_input: str) -> TurnResult:
"""跑一轮用户输入的 step 迭代,直到本轮终止。"""
...
terminated_by = "max_steps"
steps = 0
consecutive_errors = 0 for step_i in range(1, self.cfg.max_steps + 1):
steps = step_i
step_span = tracer.start_span(
"step", parent_id=None, name=f"step-{step_i}", input={"step": step_i}
) # 0) 组装 system(含记忆注入),必要时压缩历史(保头/保尾/中间摘要)
...
# 1) 调用 LLM
... # 2) 无 tool_calls → 最终答复终止
... # 3) 有 tool_calls → 逐个执行;finish 触发本轮终止
... # 容错兜底:连续失败达上限则终止本轮(成功一步清零)
... return TurnResult(
user_input=user_input,
answer=answer,
terminated_by=terminated_by,
steps=steps,
trace_path=tracer.path,
)
这里补充说明一下所使用的 API:
from openai import OpenAIself._client = OpenAI(base_url=base_url, api_key=api_key)self._client.chat.completions.create(
model=self.model,
messages=payload,
tools=tools or None,
)
其中 payload 是 list[Message],代表到目前为止的 context,也就是用户与 Agent 的对话历史,LLM 将据此给出下一个答复。tool 是 list[Tool],用于告知大模型可使用的工具列表,关于 tool use 的细节将在下一节展开。仔细分析 Message 的 role 可分为 system、user、assistant、tool 四种,它们依次交互发言:user 代表用户发言,assistant 代表大模型的回复,tool 返回工具调用结果,system 是 Agent 整理出的上下文。在 system 中可以塞入 Agent 的长期记忆、过长信息压缩后的摘要、可用的 skills 列表等内容。
@dataclass
class Message:
"""对话消息,既是短期记忆的元素,也是发给 LLM 的上下文单元。""" role: str # "system" | "user" | "assistant" | "tool"
content: Optional[str] = None # 文本内容
tool_calls: Optional[list[ToolCall]] = None # assistant 发起的工具调用
tool_call_id: Optional[str] = None # role=="tool" 时,对应的 tool_call id
name: Optional[str] = None # role=="tool" 时,工具名
Tool Use
本项目包含以下工具:
| 工具 | 作用 | 走沙箱 | 备注 |
|---|---|---|---|
run_shell |
执行 shell 命令 | 是 | 限定 ./workspace + 命令超时 |
read_file / write_file |
读 / 写工作目录内文件 | 是 | 路径限定,防越权 |
ask_user |
向用户提问(human-in-the-loop) | 否 | 回答作 observation 回灌,本轮不结束 |
finish |
显式提交最终答复、结束本轮 | 否 | |
get_current_time |
获取当前时间(local / utc) |
否 | 标准库 datetime |
http_request |
发起 HTTP(S) 请求 | 否 | 标准库 urllib,响应体过长截断 |
web_search |
Tavily 网页搜索 | 否 | 需 TAVILY_API_KEY |
remember / remember_longterm / recall |
工作 / 长期记忆读写检索 | 否 | 仅注入 memory 时注册 |
use_skill |
加载指定 skill 正文(第二层披露) | 否 | 仅注入 skills 时注册 |
以 web_search 为例,tool 本质上是一个编写好的函数,底层调用了专为大模型打造的 Tavily API(需预先注册 API Key)。代码中的 @registry.tool 是 Python 装饰器的写法,其作用是提取函数的 name、description 和 parameters,将这些信息连同函数本身一同转化为完整的 Tool 并注册到工具列表中。随后,这个工具列表会被传递给大模型,大模型再根据实际情况挑选合适的工具使用。
@registry.tool
def web_search(query: str, max_results: int = 5) -> ToolResult:
"""用 Tavily 搜索网页,返回标题、URL 与摘要。 需环境变量 TAVILY_API_KEY。 Args:
query: 搜索关键词
max_results: 返回结果条数上限
"""
...
try:
_, text = _do_http(
"https://api.tavily.com/search",
method="POST",
headers={"Content-Type": "application/json"},
data=payload,
timeout=30,
)
...
return ToolResult(ok=True, result="n".join(lines))
def tool(self, func: Callable | None = None, *, source: str = "local"):
"""装饰器:把普通函数注册为工具。 用法:``@registry.tool`` 或 ``@registry.tool(source="skill")``。
""" def wrap(fn: Callable) -> Callable:
schema = build_schema(fn)
self.register(
Tool(
name=schema["name"],
description=schema["description"],
parameters=schema["parameters"],
func=fn,
source=source,
)
)
return fn if func is not None:
return wrap(func)
return wrap
class Tool:
"""一个可被模型调用的工具。"""
name: str
description: str
parameters: dict # JSON Schema(@tool 从签名+docstring 生成,或 MCP inputSchema)
func: Callable[..., object] # 实际执行体;MCP 工具则为路由到 MCP Client 的包装
source: str = "local" # 来源分类标签:"local" | "mcp:<server>" | "skill"
# 注意:这是分类标签,与工具"名"不同;MCP 工具的 name 用 mcp__<server>__<tool> 前缀(见 3.9)
def schema(self) -> dict:
"""转成 OpenAI/DeepSeek 的 tools 数组元素。"""
return {
"type": "function",
"function": {
"name": self.name,
"description": self.description,
"parameters": self.parameters,
},
}
OpenAI 和 DeepSeek 的 API 对 Tool Use 提供了良好的支持,可以直接传入 list[Tool] 列表。如果大模型返回的 message 中发起了工具调用,那么这一信息会体现在 message.tool_calls(list[ToolCall])中,ToolCall 包含了需要调用的函数名称及其参数。Agent 这边依次完成函数调用后,会使用 message.tool_call_id 和 message.content 的形式将结果返回给大模型。
@dataclass
@dataclass
class ToolCall:
"""模型发起的一次工具调用。""" id: str # 模型给的 tool_call id,回灌时用
name: str # 工具名(本地名 或 mcp__<server>__<tool>)
arguments: dict # 已解析的入参(由 function.arguments JSON 解析而来)
@dataclass
class ToolResult:
"""工具执行的统一返回结构(3.2 错误约定)。""" ok: bool
result: Optional[object] = None # ok=True:字符串或可序列化结果
error_type: Optional[str] = None # ok=False:ValidationError|ExecutionError|TimeoutError|SandboxError
message: Optional[str] = None # ok=False:简明错误摘要
def to_content(self) -> str:
"""序列化为回灌给模型的 tool 消息文本。"""
import json if self.ok:
return json.dumps({"ok": True, "result": self.result}, ensure_ascii=False)
return json.dumps(
{"ok": False, "error_type": self.error_type, "message": self.message},
ensure_ascii=False,
)
现在我们可以把上一节 Agent Loop 里关于工具调用的部分展开。tc.name == "finish" 表示调用一种名为 finish 的特殊工具,以此作为标志来结束循环,这是一种业内常见的做法。self._dispatch 负责具体执行工具调用,如前文所述,大模型给出 tool_calls 后,Agent 按顺序依次调用。
# 3) 有 tool_calls → 逐个执行;finish 触发本轮终止
finish_answer: str | None = None
step_had_error = False
for tc in assistant_msg.tool_calls:
if tc.name == "finish":
finish_answer = str(tc.arguments.get("answer", ""))
break result = self._dispatch(tc, tracer, step_span.span_id) self.history.append(
Message(
role="tool",
content=result.to_content(),
tool_call_id=tc.id,
name=tc.name,
)
) if finish_answer is not None:
break
Context
Context 以 list[Message] 的形式存在,存储了来自 system、user、assistant、tool 的历史消息。由于大模型的上下文窗口是有限的,当 context 增大到一定程度时(以 token 数为单位,可根据大模型支持的上下文窗口大小配置阈值),就需要进行压缩处理。本项目采用的压缩策略是:保头(保留第一条用户发言)+ 保尾(保留最后 N 条)+ 中间摘要(将中间的 message 传给大模型进行压缩,然后以 role:system 的形式替换原始 messages)。
def maybe_compress(
history: list[Message],
llm: LLMClient,
cfg,
*,
extra_tokens: int = 0,
keep_recent: int | None = None,
) -> tuple[list[Message], CompressResult | None]:
"""必要时压缩 history(不含 system)。 返回 (新的 history, CompressResult 或 None)。未触发压缩时原样返回、第二项为
None。``extra_tokens`` 用于把 system 提示等固定开销计入阈值判断。 history 结构约定:首条为本会话最初的用户输入(保头),末尾 keep_recent 条为
保尾,二者之间为可压缩的中间段。
"""
keep = cfg.keep_recent if keep_recent is None else keep_recent
tokens_before = extra_tokens + estimate_messages_tokens(history)
if tokens_before <= cfg.compress_threshold:
return history, None # 保头:最初的用户输入(若存在)。保尾:最近 keep 条。
head_n = 1 if history and history[0].role == "user" else 0
if keep <= 0:
tail: list[Message] = []
middle = history[head_n:]
else:
tail = history[-keep:]
middle = history[head_n : len(history) - keep] if len(middle) <= 1:
# 中间段不足以压缩:直接返回(极小对话,等下一步再判)
return history, None # 一次额外 LLM 调用,把中间段摘要成一条
summary_resp = llm.chat(
[
Message(role="system", content=_SUMMARIZER_PROMPT),
Message(role="user", content=_serialize(middle)),
],
tools=None,
)
summary_text = (summary_resp.message.content or "").strip()
summary_text = summary_text[: cfg.summary_max_chars]
summary_msg = Message(role="system", content=f"[历史摘要]n{summary_text}")
new_history = history[:head_n] + [summary_msg] + tail
tokens_after = extra_tokens + estimate_messages_tokens(new_history)
result = CompressResult(
messages=new_history,
tokens_before=tokens_before,
tokens_after=tokens_after,
summarized_count=len(middle),
)
return new_history, result
压缩操作实际上是通过额外调用一次大模型来实现的,所使用的 prompt 如下所示。
_SUMMARIZER_PROMPT = (
"你是对话历史压缩器。请把下面这段较旧的对话历史浓缩成简短要点,"
"保留:关键事实、已确认的结论、尚未完成的待办/下一步、以及重要的工具输出结论;"
"丢弃寒暄与冗余过程。用中文,条理清晰,尽量短。"
)
接下来介绍一下第一条 system 的 message,它由三部分构成:SYSTEM_PROMPT、skills 列表以及长期记忆。skills 列表和长期记忆将在下文详细介绍。
def _system_message(self) -> Message:
"""把基础 system 提示、可用技能、工作/长期记忆合成一条 system 消息。 记忆/技能注入放在 system 里 → Context 压缩(保头)时天然保住(3.4 安全网)。
"""
text = self.system_prompt
if self.skills is not None:
skill_text = self.skills.inject_text()
if skill_text:
text = f"{text}nn{skill_text}"
if self.memory is not None:
injected = self.memory.inject_text(self.cfg.longterm_preload_n)
if injected:
text = f"{text}nn{injected}"
return Message(role="system", content=text)
DEFAULT_SYSTEM_PROMPT = (
"你是一个可以调用工具的智能助手。根据用户请求,自行决定是否调用工具:"
"简单问题直接回答即可;需要读写文件或执行命令时调用相应工具,并观察结果后继续。"
"信息不足时用 ask_user 向用户提问。完成任务后用 finish 提交最终答复,"
"或直接给出不含工具调用的普通回复作为结束。"
)
Memory
本项目的记忆系统分为三层。其中短期记忆即上一节提到的 context,包含了完整的历史消息(但有可能被压缩)。
| 层 | 内容形态 | 作用域 / 生命周期 | 存储介质 | 落盘 |
|---|---|---|---|---|
| 短期记忆 | 原始消息历史(list[Message],内容冗长,可被压缩) |
整个 session(跨轮共享) | 内存 | 否 |
| 工作记忆 | 提炼的结构化关键事实(dict[str,str],精炼稳定) |
整个 session(跨轮共享) | 内存 | 否 |
| 长期记忆 | 跨会话沉淀的知识条目(附带来源元数据) | 跨 session 持久 | JSON 文件 | 是 |
工作记忆用于提炼本 session 中的关键事实,而长期记忆则会被持久化为 JSON 文件。
class Memory:
"""工作记忆(内存)+ 长期记忆(JSON 落盘)的读写与检索。"""
def __init__(self, session_id: str, longterm_path: str) -> None:
self.session_id = session_id
self.longterm_path = longterm_path
self.working: dict[str, str] = {} # 工作记忆:session 内共享
self.longterm: list[MemoryItem] = self._load_longterm()
工作记忆的记录是通过 Tool Use 方式实现的。在对话过程中,大模型会识别是否需要将关键事实添加到工作记忆中。工作记忆和长期记忆会通过 inject_text 函数整理成字符串,然后注入到 system 的 message 里。
@registry.tool
def remember(key: str, value: str) -> ToolResult:
"""记住一条关键事实到工作记忆(本次会话内有效)。 Args:
key: 事实的键名
value: 事实的内容
"""
mem.remember(key, value) # type: ignore[attr-defined]
return ToolResult(ok=True, result=f"已记住 {key}")
def remember(self, key: str, value: str) -> None:
"""写入工作记忆(session 内有效)。"""
self.working[key] = value
# 记忆使用
def inject_text(self, preload_n: int = 5) -> str:
"""把工作记忆(全部)+ 最近 N 条长期记忆拼成一段 system 注入文本。 无内容时返回空串(调用方据此决定是否注入)。
"""
parts: list[str] = []
if self.working:
kv = "n".join(f"- {k}: {v}" for k, v in self.working.items())
parts.append(f"[工作记忆]n{kv}")
preloaded = self.preload(preload_n)
if preloaded:
lt = "n".join(f"- {it.text}" for it in preloaded)
parts.append(f"[长期记忆(最近{len(preloaded)}条)]n{lt}")
return "nn".join(parts)
同样地,长期记忆的存储和召回也是通过 Tool Use 实现的。与短期记忆和工作记忆不同,长期记忆会被持久化到文件中。如上文所述,最近 N 条长期记忆会被注入到 system 的 message 中。如有必要,大模型会调用 recall 来召回一定数量相关的记忆。当前采用的召回方法是非常简单的关键词匹配,后续可以尝试引入向量数据库,这将显著提升召回效果。
@registry.tool
def remember_longterm(text: str) -> ToolResult:
"""把一条知识沉淀到长期记忆(跨会话持久、落盘)。 Args:
text: 要长期记住的内容
"""
mem.remember_longterm(text) # type: ignore[attr-defined]
return ToolResult(ok=True, result="已写入长期记忆")
def remember_longterm(self, text: str) -> MemoryItem:
"""写入长期记忆(落盘,自动附 session_id + timestamp)。"""
item = MemoryItem(text=text, session_id=self.session_id, timestamp=time.time())
self.longterm.append(item)
self._save_longterm()
return item
@registry.tool
def recall(query: str) -> ToolResult:
"""按关键词检索长期记忆。 Args:
query: 检索关键词
"""
hits = mem.recall(query) # type: ignore[attr-defined]
if not hits:
return ToolResult(ok=True, result="(未检索到相关长期记忆)")
return ToolResult(
ok=True, result="n".join(f"- {it.text}" for it in hits)
)
def recall(self, query: str, top_k: int = 5) -> list[MemoryItem]:
"""朴素关键词包含匹配长期记忆,返回最多 top_k 条(最近优先)。"""
q = query.strip().lower()
if not q:
return []
hits = [it for it in self.longterm if q in it.text.lower()]
hits.sort(key=lambda it: it.timestamp, reverse=True)
return hits[:top_k]
trace
Agent 启动后即进入循环。每个 turn 会产生一个 trace_id,单个 turn 内的每个 step 则会产生一个 parent span 以及若干个 child span(例如上下文压缩、LLM 调用、工具调用等)。
def run_turn(self, user_input: str) -> TurnResult:
"""跑一轮用户输入的 step 迭代,直到本轮终止。"""
turn_id = _now_id("turn")
tracer = Tracer(
turn_id, self.session_id, self.cfg.trace_dir, self.cfg.trace_enabled
)
... for step_i in range(1, self.cfg.max_steps + 1):
steps = step_i
step_span = tracer.start_span(
"step", parent_id=None, name=f"step-{step_i}", input={"step": step_i}
) # 0) 组装 system(含记忆注入),必要时压缩历史(保头/保尾/中间摘要)
...
if compressed is not None:
...
tracer.end_span(
comp_span,
output={
"tokens_before": compressed.tokens_before,
"tokens_after": compressed.tokens_after,
"summarized_count": compressed.summarized_count,
},
)
... # 1) 调用 LLM
llm_span = tracer.start_span(
"llm_call",
parent_id=step_span.span_id,
input={
"messages": [_msg_to_dict(m) for m in messages],
"tools": self._tool_schemas(),
},
)
...
tracer.end_span(
llm_span,
output={"message": _msg_to_dict(resp.message), "retries": resp.retries},
) ... # 2) 无 tool_calls → 最终答复终止
... # 3) 有 tool_calls → 逐个执行;finish 触发本轮终止 for tc in assistant_msg.tool_calls:
...
fin_span = tracer.start_span(
"tool_call",
parent_id=step_span.span_id,
name="finish",
input=tc.arguments,
)
tracer.end_span(fin_span, output={"ok": True, "result": finish_answer}) ...
self.history.append(
Message(
role="tool",
content=result.to_content(),
tool_call_id=tc.id,
name=tc.name,
)
) if finish_answer is not None:
...
tracer.end_span(step_span, output={"terminated_by": terminated_by})
break ... tracer.end_span(
step_span,
output={
"tool_calls": len(assistant_msg.tool_calls),
"consecutive_errors": consecutive_errors,
},
) ... return TurnResult(
user_input=user_input,
answer=answer,
terminated_by=terminated_by,
steps=steps,
trace_path=tracer.path,
) # --- 外层 Session 循环(REPL) -------------------------------------
trace 会被落盘到 turn-xxx-xxx.json 文件中,每行记录一个 span,包含完整的用户提问、大模型调用信息、工具调用信息等。
{"trace_id": "turn-20260705-f15a27", "session_id": "sess-20260705-ab9da7", "span_id": "sp-0002", "parent_id": "sp-0001", "type": "llm_call", "name": null, "start": 1783258574.400015, "end": 1783258577.391179, "duration_ms": 2991, "input": {"messages": [{"role": "system", "content": "你是一个可以调用工具的智能助手。根据用户请求,自行决定是否调用工具:简单问题直接回答即可;需要读写文件或执行命令时调用相应工具,并观察结果后继续。信息不足时用 ask_user 向用户提问。完成任务后用 finish 提交最终答复,或直接给出不含工具调用的普通回复作为结束。nn[可用技能](需要时用 use_skill(name) 加载完整说明)n- word_count: 统计一个文本文件的行数、词数、字符数(类似 wc)"}, {"role": "user", "content": "今天深圳的天气样"}], "tools": [{"type": "function", "function": {"name": "run_shell", "description": "在沙箱工作目录中执行 shell 命令。", "parameters": {"type": "object", "properties": {"command": {"type": "string", "description": "要执行的命令"}, "timeout": {"type": "integer", "description": "超时秒数"}}, "required": ["command"]}}}, {"type": "function", "function": {"name": "read_file", "description": "读取工作目录内的文件。", "parameters": {"type": "object", "properties": {"path": {"type": "string", "description": "相对工作目录的文件路径"}}, "required": ["path"]}}}, {"type": "function", "function": {"name": "write_file", "description": "写入文件到工作目录(覆盖)。", "parameters": {"type": "object", "properties": {"path": {"type": "string", "description": "相对工作目录的文件路径"}, "content": {"type": "string", "description": "文件内容"}}, "required": ["path", "content"]}}}, {"type": "function", "function": {"name": "ask_user", "description": "当信息不足以继续时,向用户提问、请求补充信息(不结束本轮)。", "parameters": {"type": "object", "properties": {"question": {"type": "string", "description": "要问用户的问题"}}, "required": ["question"]}}}, {"type": "function", "function": {"name": "finish", "description": "提交最终答复并结束本轮。", "parameters": {"type": "object", "properties": {"answer": {"type": "string", "description": "给用户的最终答复"}}, "required": ["answer"]}}}, {"type": "function", "function": {"name": "get_current_time", "description": "获取当前日期时间。", "parameters": {"type": "object", "properties": {"tz": {"type": "string", "description": "时区,"local"(本地)或 "utc""}}, "required": []}}}, {"type": "function", "function": {"name": "http_request", "description": "发起一个 HTTP(S) 请求并返回状态码与响应体(响应体过长会截断)。", "parameters": {"type": "object", "properties": {"url": {"type": "string", "description": "请求 URL(http:// 或 https://)"}, "method": {"type": "string", "description": "HTTP 方法,如 GET / POST"}, "body": {"type": "string", "description": "请求体(POST/PUT 时使用;JSON 请自行序列化为字符串)"}, "timeout": {"type": "integer", "description": "超时秒数"}}, "required": ["url"]}}}, {"type": "function", "function": {"name": "web_search", "description": "用 Tavily 搜索网页,返回标题、URL 与摘要。", "parameters": {"type": "object", "properties": {"query": {"type": "string", "description": "搜索关键词"}, "max_results": {"type": "integer", "description": "返回结果条数上限"}}, "required": ["query"]}}}, {"type": "function", "function": {"name": "remember", "description": "记住一条关键事实到工作记忆(本次会话内有效)。", "parameters": {"type": "object", "properties": {"key": {"type": "string", "description": "事实的键名"}, "value": {"type": "string", "description": "事实的内容"}}, "required": ["key", "value"]}}}, {"type": "function", "function": {"name": "remember_longterm", "description": "把一条知识沉淀到长期记忆(跨会话持久、落盘)。", "parameters": {"type": "object", "properties": {"text": {"type": "string", "description": "要长期记住的内容"}}, "required": ["text"]}}}, {"type": "function", "function": {"name": "recall", "description": "按关键词检索长期记忆。", "parameters": {"type": "object", "properties": {"query": {"type": "string", "description": "检索关键词"}}, "required": ["query"]}}}, {"type": "function", "function": {"name": "use_skill", "description": "加载指定技能的完整操作说明(第二层渐进式披露)。", "parameters": {"type": "object", "properties": {"name": {"type": "string", "description": "技能名称(见 system 提示中的可用技能列表)"}}, "required": ["name"]}}}]}, "output": {"message": {"role": "assistant", "content": "", "tool_calls": [{"id": "call_00_ZvgizLvTBQf2kmoGXRiF8287", "name": "web_search", "arguments": {"query": "深圳今天天气", "max_results": 3}}]}, "retries": 0}, "error": null}
{"trace_id": "turn-20260705-f15a27", "session_id": "sess-20260705-ab9da7", "span_id": "sp-0003", "parent_id": "sp-0001", "type": "tool_call", "name": "web_search", "start": 1783258577.392372, "end": 1783258581.058749, "duration_ms": 3666, "input": {"query": "深圳今天天气", "max_results": 3}, "output": {"ok": true, "result": "- 深圳 - 百度n n 深圳 今天:雷阵雨 26°~31°C 南风2级. 07月05日 周日 农历五月廿一. 28°. 19 优. 小雨 ... 生活气象指数 晚上好,天气不好,也记得让心情保持舒畅哦. 穿衣: 短袖类. 26n- 深圳市气象局(台)n n [防灾减灾]( "防灾减灾"). [预警服务]( "预警服务")[预报服务]( "预报服务")[气象监测](n- 天气实况与预报-深圳市气象局(台)n n 多云;气温13-18℃;东北风3级,沿海、高地和海区阵风6级;相对湿度50%-80%。 loading. 湿度%. loading."}, "error": null}
{"trace_id": "turn-20260705-f15a27", "session_id": "sess-20260705-ab9da7", "span_id": "sp-0001", "parent_id": null, "type": "step", "name": "step-1", "start": 1783258574.399821, "end": 1783258581.060529, "duration_ms": 6660, "input": {"step": 1}, "output": {"tool_calls": 1, "consecutive_errors": 0}, "error": null}
{"trace_id": "turn-20260705-f15a27", "session_id": "sess-20260705-ab9da7", "span_id": "sp-0005", "parent_id": "sp-0004", "type": "llm_call", "name": null, "start": 1783258581.062072, "end": 1783258582.487007, "duration_ms": 1424, "input": {"messages": [{"role": "system", "content": "你是一个可以调用工具的智能助手。根据用户请求,自行决定是否调用工具:简单问题直接回答即可;需要读写文件或执行命令时调用相应工具,并观察结果后继续。信息不足时用 ask_user 向用户提问。完成任务后用 finish 提交最终答复,或直接给出不含工具调用的普通回复作为结束。nn[可用技能](需要时用 use_skill(name) 加载完整说明)n- word_count: 统计一个文本文件的行数、词数、字符数(类似 wc)"}, {"role": "user", "content": "今天深圳的天气样"}, {"role": "assistant", "content": "", "tool_calls": [{"id": "call_00_ZvgizLvTBQf2kmoGXRiF8287", "name": "web_search", "arguments": {"query": "深圳今天天气", "max_results": 3}}]}, {"role": "tool", "content": "{"ok": true, "result": "- 深圳 - 百度\n \n 深圳 今天:雷阵雨 26°~31°C 南风2级. 07月05日 周日 农历五月廿一. 28°. 19 优. 小雨 ... 生活气象指数 晚上好,天气不好,也记得让心情保持舒畅哦. 穿衣: 短袖类. 26\n- 深圳市气象局(台)\n \n [防灾减灾]( \"防灾减灾\"). [预警服务]( \"预警服务\")[预报服务]( \"预报服务\")[气象监测](\n- 天气实况与预报-深圳市气象局(台)\n \n 多云;气温13-18℃;东北风3级,沿海、高地和海区阵风6级;相对湿度50%-80%。 loading. 湿度%. loading."}", "tool_call_id": "call_00_ZvgizLvTBQf2kmoGXRiF8287", "name": "web_search"}], "tools": [{"type": "function", "function": {"name": "run_shell", "description": "在沙箱工作目录中执行 shell 命令。", "parameters": {"type": "object", "properties": {"command": {"type": "string", "description": "要执行的命令"}, "timeout": {"type": "integer", "description": "超时秒数"}}, "required": ["command"]}}}, {"type": "function", "function": {"name": "read_file", "description": "读取工作目录内的文件。", "parameters": {"type": "object", "properties": {"path": {"type": "string", "description": "相对工作目录的文件路径"}}, "required": ["path"]}}}, {"type": "function", "function": {"name": "write_file", "description": "写入文件到工作目录(覆盖)。", "parameters": {"type": "object", "properties": {"path": {"type": "string", "description": "相对工作目录的文件路径"}, "content": {"type": "string", "description": "文件内容"}}, "required": ["path", "content"]}}}, {"type": "function", "function": {"name": "ask_user", "description": "当信息不足以继续时,向用户提问、请求补充信息(不结束本轮)。", "parameters": {"type": "object", "properties": {"question": {"type": "string", "description": "要问用户的问题"}}, "required": ["question"]}}}, {"type": "function", "function": {"name": "finish", "description": "提交最终答复并结束本轮。", "parameters": {"type": "object", "properties": {"answer": {"type": "string", "description": "给用户的最终答复"}}, "required": ["answer"]}}}, {"type": "function", "function": {"name": "get_current_time", "description": "获取当前日期时间。", "parameters": {"type": "object", "properties": {"tz": {"type": "string", "description": "时区,"local"(本地)或 "utc""}}, "required": []}}}, {"type": "function", "function": {"name": "http_request", "description": "发起一个 HTTP(S) 请求并返回状态码与响应体(响应体过长会截断)。", "parameters": {"type": "object", "properties": {"url": {"type": "string", "description": "请求 URL(http:// 或 https://)"}, "method": {"type": "string", "description": "HTTP 方法,如 GET / POST"}, "body": {"type": "string", "description": "请求体(POST/PUT 时使用;JSON 请自行序列化为字符串)"}, "timeout": {"type": "integer", "description": "超时秒数"}}, "required": ["url"]}}}, {"type": "function", "function": {"name": "web_search", "description": "用 Tavily 搜索网页,返回标题、URL 与摘要。", "parameters": {"type": "object", "properties": {"query": {"type": "string", "description": "搜索关键词"}, "max_results": {"type": "integer", "description": "返回结果条数上限"}}, "required": ["query"]}}}, {"type": "function", "function": {"name": "remember", "description": "记住一条关键事实到工作记忆(本次会话内有效)。", "parameters": {"type": "object", "properties": {"key": {"type": "string", "description": "事实的键名"}, "value": {"type": "string", "description": "事实的内容"}}, "required": ["key", "value"]}}}, {"type": "function", "function": {"name": "remember_longterm", "description": "把一条知识沉淀到长期记忆(跨会话持久、落盘)。", "parameters": {"type": "object", "properties": {"text": {"type": "string", "description": "要长期记住的内容"}}, "required": ["text"]}}}, {"type": "function", "function": {"name": "recall", "description": "按关键词检索长期记忆。", "parameters": {"type": "object", "properties": {"query": {"type": "string", "description": "检索关键词"}}, "required": ["query"]}}}, {"type": "function", "function": {"name": "use_skill", "description": "加载指定技能的完整操作说明(第二层渐进式披露)。", "parameters": {"type": "object", "properties": {"name": {"type": "string", "description": "技能名称(见 system 提示中的可用技能列表)"}}, "required": ["name"]}}}]}, "output": {"message": {"role": "assistant", "content": "", "tool_calls": [{"id": "call_00_XZXGJJpFns2qCsMxtHjO9849", "name": "get_current_time", "arguments": {}}]}, "retries": 0}, "error": null}
{"trace_id": "turn-20260705-f15a27", "session_id": "sess-20260705-ab9da7", "span_id": "sp-0006", "parent_id": "sp-0004", "type": "tool_call", "name": "get_current_time", "start": 1783258582.489202, "end": 1783258582.4892821, "duration_ms": 0, "input": {}, "output": {"ok": true, "result": "2026-07-05 21:36:22 CST"}, "error": null}
{"trace_id": "turn-20260705-f15a27", "session_id": "sess-20260705-ab9da7", "span_id": "sp-0004", "parent_id": null, "type": "step", "name": "step-2", "start": 1783258581.0618382, "end": 1783258582.490081, "duration_ms": 1428, "input": {"step": 2}, "output": {"tool_calls": 1, "consecutive_errors": 0}, "error": null}
{"trace_id": "turn-20260705-f15a27", "session_id": "sess-20260705-ab9da7", "span_id": "sp-0008", "parent_id": "sp-0007", "type": "llm_call", "name": null, "start": 1783258582.4910421, "end": 1783258584.739468, "duration_ms": 2248, "input": {"messages": [{"role": "system", "content": "你是一个可以调用工具的智能助手。根据用户请求,自行决定是否调用工具:简单问题直接回答即可;需要读写文件或执行命令时调用相应工具,并观察结果后继续。信息不足时用 ask_user 向用户提问。完成任务后用 finish 提交最终答复,或直接给出不含工具调用的普通回复作为结束。nn[可用技能](需要时用 use_skill(name) 加载完整说明)n- word_count: 统计一个文本文件的行数、词数、字符数(类似 wc)"}, {"role": "user", "content": "今天深圳的天气样"}, {"role": "assistant", "content": "", "tool_calls": [{"id": "call_00_ZvgizLvTBQf2kmoGXRiF8287", "name": "web_search", "arguments": {"query": "深圳今天天气", "max_results": 3}}]}, {"role": "tool", "content": "{"ok": true, "result": "- 深圳 - 百度\n \n 深圳 今天:雷阵雨 26°~31°C 南风2级. 07月05日 周日 农历五月廿一. 28°. 19 优. 小雨 ... 生活气象指数 晚上好,天气不好,也记得让心情保持舒畅哦. 穿衣: 短袖类. 26\n- 深圳市气象局(台)\n \n [防灾减灾]( \"防灾减灾\"). [预警服务]( \"预警服务\")[预报服务]( \"预报服务\")[气象监测](\n- 天气实况与预报-深圳市气象局(台)\n \n 多云;气温13-18℃;东北风3级,沿海、高地和海区阵风6级;相对湿度50%-80%。 loading. 湿度%. loading."}", "tool_call_id": "call_00_ZvgizLvTBQf2kmoGXRiF8287", "name": "web_search"}, {"role": "assistant", "content": "", "tool_calls": [{"id": "call_00_XZXGJJpFns2qCsMxtHjO9849", "name": "get_current_time", "arguments": {}}]}, {"role": "tool", "content": "{"ok": true, "result": "2026-07-05 21:36:22 CST"}", "tool_call_id": "call_00_XZXGJJpFns2qCsMxtHjO9849", "name": "get_current_time"}], "tools": [{"type": "function", "function": {"name": "run_shell", "description": "在沙箱工作目录中执行 shell 命令。", "parameters": {"type": "object", "properties": {"command": {"type": "string", "description": "要执行的命令"}, "timeout": {"type": "integer", "description": "超时秒数"}}, "required": ["command"]}}}, {"type": "function", "function": {"name": "read_file", "description": "读取工作目录内的文件。", "parameters": {"type": "object", "properties": {"path": {"type": "string", "description": "相对工作目录的文件路径"}}, "required": ["path"]}}}, {"type": "function", "function": {"name": "write_file", "description": "写入文件到工作目录(覆盖)。", "parameters": {"type": "object", "properties": {"path": {"type": "string", "description": "相对工作目录的文件路径"}, "content": {"type": "string", "description": "文件内容"}}, "required": ["path", "content"]}}}, {"type": "function", "function": {"name": "ask_user", "description": "当信息不足以继续时,向用户提问、请求补充信息(不结束本轮)。", "parameters": {"type": "object", "properties": {"question": {"type": "string", "description": "要问用户的问题"}}, "required": ["question"]}}}, {"type": "function", "function": {"name": "finish", "description": "提交最终答复并结束本轮。", "parameters": {"type": "object", "properties": {"answer": {"type": "string", "description": "给用户的最终答复"}}, "required": ["answer"]}}}, {"type": "function", "function": {"name": "get_current_time", "description": "获取当前日期时间。", "parameters": {"type": "object", "properties": {"tz": {"type": "string", "description": "时区,"local"(本地)或 "utc""}}, "required": []}}}, {"type": "function", "function": {"name": "http_request", "description": "发起一个 HTTP(S) 请求并返回状态码与响应体(响应体过长会截断)。", "parameters": {"type": "object", "properties": {"url": {"type": "string", "description": "请求 URL(http:// 或 https://)"}, "method": {"type": "string", "description": "HTTP 方法,如 GET / POST"}, "body": {"type": "string", "description": "请求体(POST/PUT 时使用;JSON 请自行序列化为字符串)"}, "timeout": {"type": "integer", "description": "超时秒数"}}, "required": ["url"]}}}, {"type": "function", "function": {"name": "web_search", "description": "用 Tavily 搜索网页,返回标题、URL 与摘要。", "parameters": {"type": "object", "properties": {"query": {"type": "string", "description": "搜索关键词"}, "max_results": {"type": "integer", "description": "返回结果条数上限"}}, "required": ["query"]}}}, {"type": "function", "function": {"name": "remember", "description": "记住一条关键事实到工作记忆(本次会话内有效)。", "parameters": {"type": "object", "properties": {"key": {"type": "string", "description": "事实的键名"}, "value": {"type": "string", "description": "事实的内容"}}, "required": ["key", "value"]}}}, {"type": "function", "function": {"name": "remember_longterm", "description": "把一条知识沉淀到长期记忆(跨会话持久、落盘)。", "parameters": {"type": "object", "properties": {"text": {"type": "string", "description": "要长期记住的内容"}}, "required": ["text"]}}}, {"type": "function", "function": {"name": "recall", "description": "按关键词检索长期记忆。", "parameters": {"type": "object", "properties": {"query": {"type": "string", "description": "检索关键词"}}, "required": ["query"]}}}, {"type": "function", "function": {"name": "use_skill", "description": "加载指定技能的完整操作说明(第二层渐进式披露)。", "parameters": {"type": "object", "properties": {"name": {"type": "string", "description": "技能名称(见 system 提示中的可用技能列表)"}}, "required": ["name"]}}}]}, "output": {"message": {"role": "assistant", "content": "", "tool_calls": [{"id": "call_00_QncmuDfC8ucxgYxRHJ8E5459", "name": "web_search", "arguments": {"query": "深圳 2026年7月5日 天气 实时", "max_results": 3}}]}, "retries": 0}, "error": null}
{"trace_id": "turn-20260705-f15a27", "session_id": "sess-20260705-ab9da7", "span_id": "sp-0009", "parent_id": "sp-0007", "type": "tool_call", "name": "web_search", "start": 1783258584.740833, "end": 1783258587.523874, "duration_ms": 2783, "input": {"query": "深圳 2026年7月5日 天气 实时", "max_results": 3}, "output": {"ok": true, "result": "- 深圳市2026年年的過去天氣(中國) - Weather Sparkn n | 深圳市2026年的天氣歷史 中國 此報告的資料來自深圳宝安国际机场。 檢視所有附近的氣象站 溫度 31°C 熱 | | | | --- | --- | | 露點 | 26°C 令人難受 | | 相對濕度 | 75% | 降水 無報告 風 25.2 公里/小時 和風 | | | | --- | --- | | 風向 | 190 度,S | 雲量 晴時多雲 792 公尺 | | | | --- | --- | | 能見度 | 10.00 公里 | | 海拔 | 1,006 毫巴 | 原始:ZGSZ 300900Z 19007MPS 9999 FEW026 31/26 Q1006 NOSIG 此n- 深圳市在七月2026 的天氣:氣溫與氣候n n # 深圳市 在 七月 的天氣情況. | | 溫度 七月 | 27.6°C | | 降水量 七月 | 259mm |. | | 溫度 七月 最大值 | 30.3°C | | 水溫 七月 | 29°C |. | | 溫度 七月 最大值 | 30.3°C || | 溫度 七月 分鐘 | 25.7°C |. | | 溫度 七月 | 27.6°C |. 深圳市 在 七月 的溫度於 30.07 達到最高,為 30.9°C,. 在 深圳市 ,七月 的平均氣溫為 27.9°C 。水溫在前三分之一期間起初為 28.8°C ,第二個三分之一期間升溫至 28.8°C ,並於月底達到高峰 28.8°C n- 2026年深圳未来几天天气预报(7月2日至8日)n n 深圳本地宝 > 资讯快递 > 深圳城事 > 深圳天气 > 2026年深圳未来几天天气预报(7月2日至8日). 2026-07-02 10:01 作者:踩着棺材跳鬼步 【我要纠错】. 【导语】:台风结构不对称,台风外围环流下的降雨易出现列车效应、局地性强、短时雨强大,落区和强度主要靠短临监测,各位小伙伴们要注意及时关注最新天气变化!. 7月2日:多云间晴天,局地有短时(雷)阵雨,最高气温33℃左右。. 7月3日:多云,有雷阵雨,局地可达暴雨,最高气温31℃左右,沿海、高地和海区最大阵风8-9级。. 7月4-8日:阴天间多云,(雷)阵雨频密,局地(大)暴雨风险高,雷雨时伴有8级左右短时大风。. *"}, "error": null}
{"trace_id": "turn-20260705-f15a27", "session_id": "sess-20260705-ab9da7", "span_id": "sp-0007", "parent_id": null, "type": "step", "name": "step-3", "start": 1783258582.4908931, "end": 1783258587.524622, "duration_ms": 5033, "input": {"step": 3}, "output": {"tool_calls": 1, "consecutive_errors": 0}, "error": null}
{"trace_id": "turn-20260705-f15a27", "session_id": "sess-20260705-ab9da7", "span_id": "sp-0011", "parent_id": "sp-0010", "type": "llm_call", "name": null, "start": 1783258587.525333, "end": 1783258591.542552, "duration_ms": 4017, "input": {"messages": [{"role": "system", "content": "你是一个可以调用工具的智能助手。根据用户请求,自行决定是否调用工具:简单问题直接回答即可;需要读写文件或执行命令时调用相应工具,并观察结果后继续。信息不足时用 ask_user 向用户提问。完成任务后用 finish 提交最终答复,或直接给出不含工具调用的普通回复作为结束。nn[可用技能](需要时用 use_skill(name) 加载完整说明)n- word_count: 统计一个文本文件的行数、词数、字符数(类似 wc)"}, {"role": "user", "content": "今天深圳的天气样"}, {"role": "assistant", "content": "", "tool_calls": [{"id": "call_00_ZvgizLvTBQf2kmoGXRiF8287", "name": "web_search", "arguments": {"query": "深圳今天天气", "max_results": 3}}]}, {"role": "tool", "content": "{"ok": true, "result": "- 深圳 - 百度\n \n 深圳 今天:雷阵雨 26°~31°C 南风2级. 07月05日 周日 农历五月廿一. 28°. 19 优. 小雨 ... 生活气象指数 晚上好,天气不好,也记得让心情保持舒畅哦. 穿衣: 短袖类. 26\n- 深圳市气象局(台)\n \n [防灾减灾]( \"防灾减灾\"). [预警服务]( \"预警服务\")[预报服务]( \"预报服务\")[气象监测](\n- 天气实况与预报-深圳市气象局(台)\n \n 多云;气温13-18℃;东北风3级,沿海、高地和海区阵风6级;相对湿度50%-80%。 loading. 湿度%. loading."}", "tool_call_id": "call_00_ZvgizLvTBQf2kmoGXRiF8287", "name": "web_search"}, {"role": "assistant", "content": "", "tool_calls": [{"id": "call_00_XZXGJJpFns2qCsMxtHjO9849", "name": "get_current_time", "arguments": {}}]}, {"role": "tool", "content": "{"ok": true, "result": "2026-07-05 21:36:22 CST"}", "tool_call_id": "call_00_XZXGJJpFns2qCsMxtHjO9849", "name": "get_current_time"}, {"role": "assistant", "content": "", "tool_calls": [{"id": "call_00_QncmuDfC8ucxgYxRHJ8E5459", "name": "web_search", "arguments": {"query": "深圳 2026年7月5日 天气 实时", "max_results": 3}}]}, {"role": "tool", "content": "{"ok": true, "result": "- 深圳市2026年年的過去天氣(中國) - Weather Spark\n \n | 深圳市2026年的天氣歷史 中國 此報告的資料來自深圳宝安国际机场。 檢視所有附近的氣象站 溫度 31°C 熱 | | | | --- | --- | | 露點 | 26°C 令人難受 | | 相對濕度 | 75% | 降水 無報告 風 25.2 公里/小時 和風 | | | | --- | --- | | 風向 | 190 度,S | 雲量 晴時多雲 792 公尺 | | | | --- | --- | | 能見度 | 10.00 公里 | | 海拔 | 1,006 毫巴 | 原始:ZGSZ 300900Z 19007MPS 9999 FEW026 31/26 Q1006 NOSIG 此\n- 深圳市在七月2026 的天氣:氣溫與氣候\n \n # 深圳市 在 七月 的天氣情況. | | 溫度 七月 | 27.6°C | | 降水量 七月 | 259mm |. | | 溫度 七月 最大值 | 30.3°C | | 水溫 七月 | 29°C |. | | 溫度 七月 最大值 | 30.3°C || | 溫度 七月 分鐘 | 25.7°C |. | | 溫度 七月 | 27.6°C |. 深圳市 在 七月 的溫度於 30.07 達到最高,為 30.9°C,. 在 深圳市 ,七月 的平均氣溫為 27.9°C 。水溫在前三分之一期間起初為 28.8°C ,第二個三分之一期間升溫至 28.8°C ,並於月底達到高峰 28.8°C \n- 2026年深圳未来几天天气预报(7月2日至8日)\n \n 深圳本地宝 > 资讯快递 > 深圳城事 > 深圳天气 > 2026年深圳未来几天天气预报(7月2日至8日). 2026-07-02 10:01 作者:踩着棺材跳鬼步 【我要纠错】. 【导语】:台风结构不对称,台风外围环流下的降雨易出现列车效应、局地性强、短时雨强大,落区和强度主要靠短临监测,各位小伙伴们要注意及时关注最新天气变化!. 7月2日:多云间晴天,局地有短时(雷)阵雨,最高气温33℃左右。. 7月3日:多云,有雷阵雨,局地可达暴雨,最高气温31℃左右,沿海、高地和海区最大阵风8-9级。. 7月4-8日:阴天间多云,(雷)阵雨频密,局地(大)暴雨风险高,雷雨时伴有8级左右短时大风。. *"}", "tool_call_id": "call_00_QncmuDfC8ucxgYxRHJ8E5459", "name": "web_search"}], "tools": [{"type": "function", "function": {"name": "run_shell", "description": "在沙箱工作目录中执行 shell 命令。", "parameters": {"type": "object", "properties": {"command": {"type": "string", "description": "要执行的命令"}, "timeout": {"type": "integer", "description": "超时秒数"}}, "required": ["command"]}}}, {"type": "function", "function": {"name": "read_file", "description": "读取工作目录内的文件。", "parameters": {"type": "object", "properties": {"path": {"type": "string", "description": "相对工作目录的文件路径"}}, "required": ["path"]}}}, {"type": "function", "function": {"name": "write_file", "description": "写入文件到工作目录(覆盖)。", "parameters": {"type": "object", "properties": {"path": {"type": "string", "description": "相对工作目录的文件路径"}, "content": {"type": "string", "description": "文件内容"}}, "required": ["path", "content"]}}}, {"type": "function", "function": {"name": "ask_user", "description": "当信息不足以继续时,向用户提问、请求补充信息(不结束本轮)。", "parameters": {"type": "object", "properties": {"question": {"type": "string", "description": "要问用户的问题"}}, "required": ["question"]}}}, {"type": "function", "function": {"name": "finish", "description": "提交最终答复并结束本轮。", "parameters": {"type": "object", "properties": {"answer": {"type": "string", "description": "给用户的最终答复"}}, "required": ["answer"]}}}, {"type": "function", "function": {"name": "get_current_time", "description": "获取当前日期时间。", "parameters": {"type": "object", "properties": {"tz": {"type": "string", "description": "时区,"local"(本地)或 "utc""}}, "required": []}}}, {"type": "function", "function": {"name": "http_request", "description": "发起一个 HTTP(S) 请求并返回状态码与响应体(响应体过长会截断)。", "parameters": {"type": "object", "properties": {"url": {"type": "string", "description": "请求 URL(http:// 或 https://)"}, "method": {"type": "string", "description": "HTTP 方法,如 GET / POST"}, "body": {"type": "string", "description": "请求体(POST/PUT 时使用;JSON 请自行序列化为字符串)"}, "timeout": {"type": "integer", "description": "超时秒数"}}, "required": ["url"]}}}, {"type": "function", "function": {"name": "web_search", "description": "用 Tavily 搜索网页,返回标题、URL 与摘要。", "parameters": {"type": "object", "properties": {"query": {"type": "string", "description": "搜索关键词"}, "max_results": {"type": "integer", "description": "返回结果条数上限"}}, "required": ["query"]}}}, {"type": "function", "function": {"name": "remember", "description": "记住一条关键事实到工作记忆(本次会话内有效)。", "parameters": {"type": "object", "properties": {"key": {"type": "string", "description": "事实的键名"}, "value": {"type": "string", "description": "事实的内容"}}, "required": ["key", "value"]}}}, {"type": "function", "function": {"name": "remember_longterm", "description": "把一条知识沉淀到长期记忆(跨会话持久、落盘)。", "parameters": {"type": "object", "properties": {"text": {"type": "string", "description": "要长期记住的内容"}}, "required": ["text"]}}}, {"type": "function", "function": {"name": "recall", "description": "按关键词检索长期记忆。", "parameters": {"type": "object", "properties": {"query": {"type": "string", "description": "检索关键词"}}, "required": ["query"]}}}, {"type": "function", "function": {"name": "use_skill", "description": "加载指定技能的完整操作说明(第二层渐进式披露)。", "parameters": {"type": "object", "properties": {"name": {"type": "string", "description": "技能名称(见 system 提示中的可用技能列表)"}}, "required": ["name"]}}}]}, "output": {"message": {"role": "assistant", "content": "好的,我帮你查到了深圳今天(2026年7月5日)的天气情况 ️️nn## 深圳今日天气nn| 项目 | 详情 |n|------|------|n| **天气** | **雷阵雨 / 阴天间多云,有(雷)阵雨** |n| **温度** | **26°C ~ 31°C** |n| **风** | 南风 2 级 |n| ️ **提醒** | 局地可能有(大)暴雨风险,雷雨时伴有 8 级左右短时大风 |nn### 温馨提示n- 今天深圳受台风外围环流影响,**雷阵雨比较频密**,出门建议 **带伞** ️n- 短时雨势可能较大,注意防范局地暴雨和短时大风n- 温度虽然不算太高,但湿度较大(约75%左右),体感会比较闷热,建议穿 **短袖类衣物**nn祝你在深圳度过愉快的一天!"}, "retries": 0}, "error": null}
{"trace_id": "turn-20260705-f15a27", "session_id": "sess-20260705-ab9da7", "span_id": "sp-0010", "parent_id": null, "type": "step", "name": "step-4", "start": 1783258587.5252202, "end": 1783258591.544246, "duration_ms": 4019, "input": {"step": 4}, "output": {"terminated_by": "final_answer"}, "error": null}
skill
Skill 通常包含一个 SKILL.md 文件,以及多个脚本和资源文件。SKILL.md 文件一般包含 name、description 和详细内容。实际上,Skill 可以被视为一段提示词,用于帮助大模型完成特定的任务。
---
name: word_count
description: 统计一个文本文件的行数、词数、字符数(类似 wc)
---## 用途统计工作目录内某个文本文件的行数、单词数、字符数。## 步骤1. 确认要统计的文件已在工作目录(`./workspace`)内;若没有,先用 `write_file` 创建。
2. 用 `run_shell` 调用本技能脚本: ```text python3 ../skills/word_count/scripts/count.py <文件名> ``` 注意:`run_shell` 的工作目录是 `./workspace`,脚本在其上一级的 `skills/` 下, 因此用相对路径 `../skills/word_count/scripts/count.py`。
3. 脚本会输出形如 `lines=.. words=.. chars=..` 的一行结果,把它作为最终答复。
```
如前文所述,可用skill列表会被注入system的message中。这里实惠注入name和description。
```
def inject_text(self) -> str:
"""第一层披露:把所有 skill 的 name+description 拼成 system 注入文本。"""
if not self.skills:
return ""
lines = [f"- {s.name}: {s.description}" for s in self.skills.values()]
return (
"[可用技能](需要时用 use_skill(name) 加载完整说明)n" + "n".join(lines)
)
本项目会在启动时通过 scan 扫描 skill 目录,并通过 inject_text 将所有可用 SKILL.md 的 name 和 description 加载到 system 的 prompt 中。为了节省 token,这里并没有把完整的 SKILL.md 内容加载进去。当大模型判断需要使用某个 skill 时,通过 Tool Use 的方式调用 use_skill 这个 tool(底层函数是 load_body),从而实现懒加载 skill 的完整内容。
class SkillManager:
"""扫描并持有 skills;提供 system 注入文本与正文加载。"""
def __init__(self, skills_dir: str, body_max_chars: int = 8000) -> None:
self.skills_dir = skills_dir
self.body_max_chars = body_max_chars
self.skills: dict[str, Skill] = self._scan()
def _scan(self) -> dict[str, Skill]:
out: dict[str, Skill] = {}
if not os.path.isdir(self.skills_dir):
return out
for name in sorted(os.listdir(self.skills_dir)):
sdir = os.path.join(self.skills_dir, name)
md = os.path.join(sdir, "SKILL.md")
if not os.path.isfile(md):
continue
with open(md, encoding="utf-8") as f:
meta, _ = _parse_frontmatter(f.read())
out[meta.get("name", name)] = Skill(
name=meta.get("name", name),
description=meta.get("description", ""),
path=md,
dir=sdir,
)
return out
def inject_text(self) -> str:
"""第一层披露:把所有 skill 的 name+description 拼成 system 注入文本。"""
if not self.skills:
return ""
lines = [f"- {s.name}: {s.description}" for s in self.skills.values()]
return (
"[可用技能](需要时用 use_skill(name) 加载完整说明)n" + "n".join(lines)
)
def load_body(self, name: str) -> str | None:
"""第二层披露:读取指定 skill 的 SKILL.md 正文(超限截断)。"""
skill = self.skills.get(name)
if skill is None:
return None
with open(skill.path, encoding="utf-8") as f:
_, body = _parse_frontmatter(f.read())
return body.strip()[: self.body_max_chars]
use_skill 的具体实现如下:
@registry.tool
def use_skill(name: str) -> ToolResult:
"""加载指定技能的完整操作说明(第二层渐进式披露)。 Args:
name: 技能名称(见 system 提示中的可用技能列表)
"""
body = skills.load_body(name) # type: ignore[attr-defined]
if body is None:
return ToolResult(
ok=False,
error_type="ExecutionError",
message=f"未知技能: {name}",
)
return ToolResult(ok=True, result=body)
MCP
MCP 可以被视为另一种类型的 Tool,Agent 可以连接本地或远程的 MCP Server 来拓展自身能力。本项目仅实现了基于本地 stdio 传输的 MCP Client。Agent 启动时,会逐个启动 MCP Client,尝试向 MCP Server 发起 tools/list 请求,获取可用的工具列表,然后将这些工具包装成 Tool。后续的流程与普通的 Tool Use 完全一致。
def list_tools(self) -> list[Tool]:
"""tools/list → 包装为 Tool(名带 mcp__<server>__ 前缀)。"""
resp = self._request("tools/list", {})
tools: list[Tool] = []
for t in resp.get("tools", []):
name = f"mcp__{self.server_name}__{t['name']}"
tools.append(
Tool(
name=name,
description=t.get("description", ""),
parameters=t.get("inputSchema", {"type": "object", "properties": {}}),
func=self._make_caller(t["name"]),
source=f"mcp:{self.server_name}",
)
)
return tools
沙箱
Agent 会执行模型所决定的命令和代码。沙箱的作用正是限制这些不确定操作在文件、网络、资源和权限方面的边界,从而避免误删、泄密、污染环境、资源耗尽,以及防止恶意代码影响宿主机。
def run_shell(self, command: str, timeout: int) -> ToolResult:
limit = min(timeout, self.command_timeout)
try:
proc = subprocess.run(
["docker", "exec", self.container, "sh", "-c", command],
capture_output=True,
text=True,
timeout=limit,
)
except subprocess.TimeoutExpired:
return ToolResult(
ok=False, error_type="TimeoutError", message=f"命令超时(>{limit}s)"
)
out = (proc.stdout or "") + (proc.stderr or "")
if proc.returncode != 0:
return ToolResult(
ok=False,
error_type="ExecutionError",
message=f"退出码 {proc.returncode}: {out.strip()[:500]}",
)
return ToolResult(ok=True, result=out.strip())
eval
Eval 指的是使用一组任务用例来自动化衡量 Agent 的表现,例如任务通过率等指标。下面是一个最简单的用例示例。
{
"id": "smoke_contains",
"input": "请直接回复一句话,其中必须包含 HARNESS_OK 这个词,不要调用任何工具。",
"answers": [],
"checks": [
{"type": "contains", "value": "HARNESS_OK"}
]
}
用例执行结果会包含是否通过、耗时等信息。
{
"total": 1,
"passed": 1,
"pass_rate": 1.0,
"total_ms": 2025,
"results": [
{
"id": "smoke_contains",
"passed": true,
"elapsed_ms": 2022,
"terminated_by": "final_answer",
"answer": "HARNESS_OK 已确认,一切正常。",
"trace_path": "./traces/sess-20260705-42fa18/turn-20260705-8b583a.jsonl"
}
]
}
运行一个用例,本质上就是启动一个 Agent,然后回答一个问题,同时记录中间的各项指标,并判定最终结果是否符合预期。
def run_case(case: dict, cfg: Config, llm: LLMClient, tmp_root: str) -> dict:
"""跑单个用例:干净沙箱 + 独立空长期记忆,返回结果 dict。"""
case_id = case.get("id", "<no-id>")
workspace = os.path.join(tmp_root, case_id, "workspace")
longterm = os.path.join(tmp_root, case_id, "long_term.json")
os.makedirs(workspace, exist_ok=True) provider = ScriptedInputProvider(case.get("answers", []))
agent = Agent(cfg, llm=llm, input_provider=provider)
memory = Memory(agent.session_id, longterm) # 独立空长期记忆
registry = ToolRegistry()
ctx = ToolContext(
workspace_dir=workspace,
command_timeout=cfg.command_timeout,
input_provider=provider,
memory=memory,
)
register_builtins(registry, ctx)
agent.registry = registry
agent.memory = memory start = time.time()
result = agent.run_turn(case["input"])
elapsed = time.time() - start checks = case.get("checks", [])
passed = all(_check(c, result, workspace) for c in checks)
return {
"id": case_id,
"passed": passed,
"elapsed_ms": int(elapsed * 1000),
"terminated_by": result.terminated_by,
"answer": result.answer,
"trace_path": result.trace_path,
}
截至目前,本项目所做的 eval 相对较少。除此之外,Eval 还可以检测但不限于以下指标:
LLM 推理
ToolUse
Agent Loop
错误自愈
HITL
Sandbox
Memory
Context 压缩
Trace
最终输出
通过以上完整实现,您将可以独立搭建一个具备核心能力的 Agent 系统,并深入理解其背后各个模块的设计思想与协同工作方式。