目标
这是一个教学项目,包含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。
text
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 的循环。
python
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执行返回给大模型-大模型给出下一步行动,直至任务完成。
ini
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
ini
from openai import OpenAI
self._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是listMessage,这部分是到目前为止的context,也就是用户和agent的对话历史,LLM将依此给出下一个答复。tool是listTool,这是告诉大模型可以使用的tool列表,关于tool use的细节将在下一节展开。细看Message的role分为system、user、assistant、tool,四者交互发言,user即用户的发言,assistant是大模型的回复,tool返回工具调用结果。system是agent整理的上下文,这里可以塞入:agent长期记忆、过长的信息压缩后的摘要、可用的skills列表等等。
python
@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注册到工具列表里,后续再把这个工具列表传给大模型。随后大模型根据情况挑选tool使用。
python
@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有很好的支持,可以直接传listTool列表。如果大模型返回的message提出发起工具调用,则会体现在message.tool_calls(listToolCall),ToolCall包含调用哪个函数和参数。agent这边依次完成函数调用后,使用message.tool_call_id & message.content的形式返回给大模型。
python
@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,以此为标志结束loop,这是一种业内常见做法。self._dispatch是具体执行工具调用,如上文介绍的,大模型给出tool_calls,agent按顺序依次调用。
ini
# 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是以listMessage的形式存在的,存了来自system/user/assitant/tool的历史消息。大模型的上下文窗口是有限的,所以当context大到一定程度时(以token数为单位,可以按大模型支持的上下文窗口大小配置阈值),需要压缩。本项目使用的压缩策略是保头(保留第一条用户发言) + 保尾(保留最后N条) + 中间摘要(中间的message传给大模型压缩,然后以role:system的形式替换原始messages)
ini
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如下。
makefile
_SUMMARIZER_PROMPT = (
"你是对话历史压缩器。请把下面这段较旧的对话历史浓缩成简短要点,"
"保留:关键事实、已确认的结论、尚未完成的待办/下一步、以及重要的工具输出结论;"
"丢弃寒暄与冗余过程。用中文,条理清晰,尽量短。"
)
再介绍一下开头第一条system的message,由三部分沟通:SYSTEM_PROMPT + skills列表 + 长期记忆。skills列表 & 长期记忆将在下文介绍。
python
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}\n\n{skill_text}"
if self.memory is not None:
injected = self.memory.inject_text(self.cfg.longterm_preload_n)
if injected:
text = f"{text}\n\n{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文件。
python
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里。
python
@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 "\n\n".join(parts)
同理,长期记忆的存储和召回也是通过tool use实现。跟短期记忆和工作记忆不同,长期记忆会被持久化道文件里。如上文介绍,最近N条长期记忆会被注入system的message中。如有必要,大模型会调用recall再召回一定数量相关的记忆。当前的召回用了非常简单的关键词匹配,可以尝试引入向量数据库,这样将会提升召回效果。
python
@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被打开后,就会进入loop。每个turn会产生一个trace_id,单个turn里面的每个step会产生一个1个parent span以及若干个child span(压缩上下文、调用llm、工具调用等等)
ini
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,包含完整的用户提问、大模型调用信息、工具调用信息等。
perl
{"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 提交最终答复,或直接给出不含工具调用的普通回复作为结束。\n\n[可用技能](需要时用 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 http://weathernew.pae.baidu.com/weathernew/pc?query=%E5%B9%BF%E4%B8%9C%E6%B7%B1%E5%9C%B3%E5%A4%A9%E6%B0%94&srcid=4982&city_name=%E6%B7%B1%E5%9C%B3&province_name=%E5%B9%BF%E4%B8%9C\n 深圳 今天:雷阵雨 26°~31°C 南风2级. 07月05日 周日 农历五月廿一. 28°. 19 优. 小雨 ... 生活气象指数 晚上好,天气不好,也记得让心情保持舒畅哦. 穿衣: 短袖类. 26\n- 深圳市气象局(台)\n https://weather.sz.gov.cn\n [防灾减灾](https://weather.sz.gov.cn/qixiangfuwu/fangzaijianzai/fangzaijianzai/index.html \"防灾减灾\"). [预警服务](https://weather.sz.gov.cn/qixiangfuwu/yujingfuwu/index.html \"预警服务\")[预报服务](https://weather.sz.gov.cn/qixiangfuwu/yubaofuwu/index.html \"预报服务\")[气象监测](https://weather.sz.gov.cn/qixiangfuwu/qixiangjiance\n- 天气实况与预报-深圳市气象局(台)\n https://weather.sz.gov.cn/mobile/qixiangfuwu/yubaofuwu/jinmingtianqiyubao\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 提交最终答复,或直接给出不含工具调用的普通回复作为结束。\n\n[可用技能](需要时用 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 http://weathernew.pae.baidu.com/weathernew/pc?query=%E5%B9%BF%E4%B8%9C%E6%B7%B1%E5%9C%B3%E5%A4%A9%E6%B0%94&srcid=4982&city_name=%E6%B7%B1%E5%9C%B3&province_name=%E5%B9%BF%E4%B8%9C\\n 深圳 今天:雷阵雨 26°~31°C 南风2级. 07月05日 周日 农历五月廿一. 28°. 19 优. 小雨 ... 生活气象指数 晚上好,天气不好,也记得让心情保持舒畅哦. 穿衣: 短袖类. 26\\n- 深圳市气象局(台)\\n https://weather.sz.gov.cn\\n [防灾减灾](https://weather.sz.gov.cn/qixiangfuwu/fangzaijianzai/fangzaijianzai/index.html \\\"防灾减灾\\\"). [预警服务](https://weather.sz.gov.cn/qixiangfuwu/yujingfuwu/index.html \\\"预警服务\\\")[预报服务](https://weather.sz.gov.cn/qixiangfuwu/yubaofuwu/index.html \\\"预报服务\\\")[气象监测](https://weather.sz.gov.cn/qixiangfuwu/qixiangjiance\\n- 天气实况与预报-深圳市气象局(台)\\n https://weather.sz.gov.cn/mobile/qixiangfuwu/yubaofuwu/jinmingtianqiyubao\\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 提交最终答复,或直接给出不含工具调用的普通回复作为结束。\n\n[可用技能](需要时用 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 http://weathernew.pae.baidu.com/weathernew/pc?query=%E5%B9%BF%E4%B8%9C%E6%B7%B1%E5%9C%B3%E5%A4%A9%E6%B0%94&srcid=4982&city_name=%E6%B7%B1%E5%9C%B3&province_name=%E5%B9%BF%E4%B8%9C\\n 深圳 今天:雷阵雨 26°~31°C 南风2级. 07月05日 周日 农历五月廿一. 28°. 19 优. 小雨 ... 生活气象指数 晚上好,天气不好,也记得让心情保持舒畅哦. 穿衣: 短袖类. 26\\n- 深圳市气象局(台)\\n https://weather.sz.gov.cn\\n [防灾减灾](https://weather.sz.gov.cn/qixiangfuwu/fangzaijianzai/fangzaijianzai/index.html \\\"防灾减灾\\\"). [预警服务](https://weather.sz.gov.cn/qixiangfuwu/yujingfuwu/index.html \\\"预警服务\\\")[预报服务](https://weather.sz.gov.cn/qixiangfuwu/yubaofuwu/index.html \\\"预报服务\\\")[气象监测](https://weather.sz.gov.cn/qixiangfuwu/qixiangjiance\\n- 天气实况与预报-深圳市气象局(台)\\n https://weather.sz.gov.cn/mobile/qixiangfuwu/yubaofuwu/jinmingtianqiyubao\\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 Spark\n https://tw.weatherspark.com/h/y/127893/2026/%E6%B7%B1%E5%9C%B3%E5%B8%82%E3%80%81%E4%B8%AD%E5%9C%8B2026%E5%B9%B4%E6%AD%B7%E5%8F%B2%E5%A4%A9%E6%B0%A3\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 https://zh.climate-data.org/%E4%BA%9E%E6%B4%B2/%E4%B8%AD%E5%9B%BD/%E5%B9%BF%E4%B8%9C%E7%9C%81/%E6%B7%B1%E5%9C%B3%E5%B8%82-1064927/t/%E4%B8%83%E6%9C%88-7\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 http://sz.bendibao.com/news/202672/1007165.htm\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 提交最终答复,或直接给出不含工具调用的普通回复作为结束。\n\n[可用技能](需要时用 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 http://weathernew.pae.baidu.com/weathernew/pc?query=%E5%B9%BF%E4%B8%9C%E6%B7%B1%E5%9C%B3%E5%A4%A9%E6%B0%94&srcid=4982&city_name=%E6%B7%B1%E5%9C%B3&province_name=%E5%B9%BF%E4%B8%9C\\n 深圳 今天:雷阵雨 26°~31°C 南风2级. 07月05日 周日 农历五月廿一. 28°. 19 优. 小雨 ... 生活气象指数 晚上好,天气不好,也记得让心情保持舒畅哦. 穿衣: 短袖类. 26\\n- 深圳市气象局(台)\\n https://weather.sz.gov.cn\\n [防灾减灾](https://weather.sz.gov.cn/qixiangfuwu/fangzaijianzai/fangzaijianzai/index.html \\\"防灾减灾\\\"). [预警服务](https://weather.sz.gov.cn/qixiangfuwu/yujingfuwu/index.html \\\"预警服务\\\")[预报服务](https://weather.sz.gov.cn/qixiangfuwu/yubaofuwu/index.html \\\"预报服务\\\")[气象监测](https://weather.sz.gov.cn/qixiangfuwu/qixiangjiance\\n- 天气实况与预报-深圳市气象局(台)\\n https://weather.sz.gov.cn/mobile/qixiangfuwu/yubaofuwu/jinmingtianqiyubao\\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 https://tw.weatherspark.com/h/y/127893/2026/%E6%B7%B1%E5%9C%B3%E5%B8%82%E3%80%81%E4%B8%AD%E5%9C%8B2026%E5%B9%B4%E6%AD%B7%E5%8F%B2%E5%A4%A9%E6%B0%A3\\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 https://zh.climate-data.org/%E4%BA%9E%E6%B4%B2/%E4%B8%AD%E5%9B%BD/%E5%B9%BF%E4%B8%9C%E7%9C%81/%E6%B7%B1%E5%9C%B3%E5%B8%82-1064927/t/%E4%B8%83%E6%9C%88-7\\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 http://sz.bendibao.com/news/202672/1007165.htm\\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日)的天气情况 ☀️🌧️\n\n## 📍 深圳今日天气\n\n| 项目 | 详情 |\n|------|------|\n| 🌤 **天气** | **雷阵雨 / 阴天间多云,有(雷)阵雨** |\n| 🌡 **温度** | **26°C ~ 31°C** |\n| 💨 **风** | 南风 2 级 |\n| ⚠️ **提醒** | 局地可能有(大)暴雨风险,雷雨时伴有 8 级左右短时大风 |\n\n### 🗒 温馨提示\n- 今天深圳受台风外围环流影响,**雷阵雨比较频密**,出门建议 **带伞** ☂️\n- 短时雨势可能较大,注意防范局地暴雨和短时大风\n- 温度虽然不算太高,但湿度较大(约75%左右),体感会比较闷热,建议穿 **短袖类衣物**\n\n祝你在深圳度过愉快的一天!😊"}, "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其实可以视为一段提示词,帮助大模型完成特定任务。
yaml
---
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),懒加载kill完整内容。
python
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的具体实现
python
@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,后续的流程就跟普通的tool use一模一样了。
python
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会执行模型决定的命令和代码;沙箱用来限制这些不确定动作的文件、网络、资源和权限边界,避免误删、泄密、污染环境、资源耗尽和恶意代码影响宿主机。
ini
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 表现(如任务通过率)。下面是一个最简单的用例。
json
{
"id": "smoke_contains",
"input": "请直接回复一句话,其中必须包含 HARNESS_OK 这个词,不要调用任何工具。",
"answers": [],
"checks": [
{"type": "contains", "value": "HARNESS_OK"}
]
}
用例执行结果包含是否通过、耗时等信息
json
{
"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,然后回答一个问题,然后把中间指标记录、判定结果是否符合预期。
ini
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还可以检测包括但不限于以下指标。
vbnet
LLM 推理
ToolUse
Agent Loop
错误自愈
HITL
Sandbox
Memory
Context 压缩
Trace
最终输出