从0开始实现个人最简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 的工具并入统一注册表,与本地工具同链路调用。

前置工作

设计细节

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
最终输出
相关推荐
付玉祥1 小时前
给 Agent 加一个 Approval Gate
agent
项目治理之道1 小时前
项目管理中的“附件黑洞”:VisualProject如何让附件从“信息孤岛”蜕变为核心资产
人工智能·项目管理工具·项目管理软件·金融科技管理
东风破_1 小时前
Tool Use 的核心:模型只负责判断,Runtime 才真正执行
人工智能
To_OC1 小时前
我写了个 10 行的加法函数,终于搞懂了什么是 Harness Engineering
人工智能·llm·claude
是烨笙啊1 小时前
从Loop Engineering看AI圈的循环营销术:万物皆可“工程”
人工智能
大模型最新论文速读1 小时前
RL 的 token 选择之争:高熵词到底重不重要?
论文阅读·人工智能·深度学习·神经网络·机器学习
雪隐1 小时前
用Flutter做背单词APP-00前言
前端·人工智能·后端
QiLinkOS1 小时前
第三视觉理解徐玉生与他的商业活动(32)
大数据·c++·人工智能·算法·开源协议
孤狼GPT2 小时前
2026年7月怎么充GPT会员?先分清Plus、Pro、Codex和API
人工智能·gpt·chatgpt·codex