重塑深度智能体中的技能

Deep Agents 为 Agent Skills 加入工具绑定、技能固定与线程中途重载,让数千技能库可规模化且不挤占上下文。

SYDNEY RUNKLE(@SYDNEYRUNKLE)教程 · 已翻译 · 约 16 分钟
阅览室 · AI 最佳实践#技能#智能体#工具#固定#绑定#Agent#线程原文

技能是为智能体赋予领域知识的最佳方式之一。一个技能就是一个包含指令、脚本和参考文件的文件夹,它教会智能体如何做事,比如按照你们销售团队的方式准备客户会议或审阅通话记录。Agent Skills 是一项开放标准,适用于任何模型,并受到数十种智能体产品的支持。你也不需要具备技术背景就能编写一个技能:从本质上讲,技能就是一个 markdown 文件。

技能之所以有效,是因为渐进式披露。智能体一开始只能看到每个技能的名称和描述,只有当任务需要时才会读取完整的指令。这样可以让上下文保持精简,而上下文工程正是构建高效智能体的关键。

随着使用规模不断扩大,团队对技能的需求也在发生变化。我们看到企业技能注册表已增长到数千个技能,在团队和智能体之间共享。我们改进了 Deep Agents 中的技能支持,以回应一些常见需求:

  • 将工具绑定到技能: 绑定到某个技能的工具只有在智能体读取该技能时才会加载。
  • 固定技能: 当用户明确请求某个技能时,比如 /meeting-prep,你的应用可以在下一次模型调用之前加载它。
  • 技能重新加载: 长时间运行的线程可以获取新增或变更的技能,而无需重新开始。

技能如何运作

一个技能就是一个包含 SKILL.md 文件的目录:YAML frontmatter 中有 name 和 description,后面是 agent 遵循的指令。一个技能还可以在 scripts/、references/ 和 assets/ 下捆绑支持文件(规范)。

只有技能的名称和描述始终在上下文中。智能体按需读取指令,仅在需要时加载脚本、参考文件和资产。
只有技能的名称和描述始终在上下文中。智能体按需读取指令,仅在需要时加载脚本、参考文件和资产。

只有技能的 name 和 description 始终在上下文中。agent 按需读取指令,仅在需要时才加载 scripts、references 和 assets。

在本文中,我们将以我们的 GTM agent 作为贯穿始终的示例。它构建于 Deep Agents 之上,其包含 50 多个技能的库覆盖了销售代表的日常工作,例如 meeting-prep、call-transcripts 和 competitive-intel-card。

技能分三个层级加载:

  1. 发现。 启动时,agent 在其系统提示中看到每个技能的 name 和 description。
  2. 激活。 当任务匹配某个技能时,agent 用 read_file 读取完整的 SKILL.md。
  3. 执行。 agent 遵循指令,仅在需要时才读取 scripts 或 reference 文件。
智能体的上下文只随任务所需而增长:启动时每个技能的名称和描述,然后是一个技能的指令,再然后是一个参考文件。
智能体的上下文只随任务所需而增长:启动时每个技能的名称和描述,然后是一个技能的指令,再然后是一个参考文件。

agent 的上下文只随任务所需而增长:启动时是每个技能的 name 和 description,然后是一个技能的指令,再然后是一个 reference 文件。

在技能被使用之前,它只占用系统提示中的一行,因此一个技能库可以容纳大量技能的引用,而不会挤占上下文。现在,让我们来看看我们在 Deep Agents 中做出的增强。

将工具绑定到技能

技能通常会告诉智能体如何使用特定的工具,而有些工具只有在智能体阅读了这些说明之后才能很好地工作。在此之前,技能和工具是分别披露的。你可以通过工具搜索将工具 schema 排除在上下文之外,但没有任何机制将工具与解释它的技能绑定在一起:智能体可能找到并调用某个工具,却没有阅读它的技能;或者阅读了技能,却仍然不得不去搜索它的工具。

现在你可以将工具绑定到技能,这样技能及其工具就会一起披露。绑定的工具在智能体阅读其技能之前不会被加入上下文,在此之前调用它会因工具未知而失败。这既保持了上下文的精简,也意味着智能体在能够调用某个工具之前,已经阅读了如何使用它。在我们的 GTM 智能体中,call-transcripts 说明了如何搜索通话和阅读转录文本,因此它是绑定这些工具的天然位置。

在技能 frontmatter 的 metadata.include_tools 下列出这些工具:

name: call-transcripts
description: Find and read customer call transcripts. Use when a rep asks what was said on a call or needs context from past conversations.
metadata:
  include_tools: search_calls get_transcript

将这些工具传给 SkillsMiddleware,而不是传给智能体。在智能体阅读 call-transcripts 之前,它们会一直保持隐藏:

from deepagents import create_deep_agent
from deepagents.middleware import SkillsMiddleware

agent = create_deep_agent(
    model="anthropic:claude-sonnet-4-6",
    backend=backend,
    middleware=[
        SkillsMiddleware(
            backend=backend,
            sources=["/skills/"],
            tools=[search_calls, get_transcript],
        ),
    ],
)
读取通话记录会解锁 search_calls 和 get_transcript。它们出现在新的系统消息中,因此其上方的缓存前缀保持不变。
读取通话记录会解锁 search_calls 和 get_transcript。它们出现在新的系统消息中,因此其上方的缓存前缀保持不变。

读取调用记录会解锁 search_calls 和 get_transcript。它们会出现在一条新的系统消息中,因此其上方已缓存的前缀保持不变。

过去,在对话进行到一半时添加工具意味着要修改请求的工具列表,这会使提示缓存失效。Anthropic 和 OpenAI 现在允许较新的模型在对话中途接受工具,因此在这些模型上,Deep Agents 会在技能被读取后立即添加该技能绑定的工具,而已缓存的前缀保持不变(参见 Anthropic 和 OpenAI 集成文档)。在其他模型上,工具仍像以前一样追加到请求中。

一个列表就能覆盖大多数技能。若需要更多控制,技能可以列出一个标签而非工具名称,而你传给 SkillsMiddleware 的函数会将每个标签转换为工具。这样一来,你可以:

  • 一次性披露整个工具组,比如某个 MCP 服务器上的所有工具,只需一个名称,而无需在技能中逐一列出每个工具。
  • 根据运行时权限来限制工具。 该函数会接收图的运行时,因此它可以检查用户是谁,并只返回该用户被允许使用的工具。

在这里,call-transcripts 获得了 calls MCP 服务器上的所有工具,而 pipeline-forecast 获得了 CRM 工具,但只有经理才能更新预测:

# call-transcripts/SKILL.md
metadata:
  include_tools: call_tools

# pipeline-forecast/SKILL.md
metadata:
  include_tools: crm_tools
call_tools = await mcp_client.get_tools(server_name="calls")

def resolve_skill_tools(name: str, runtime: Runtime) -> list[BaseTool]:
    if name == "call_tools":
        return call_tools
    if name == "crm_tools":
        if "manager" in runtime.server_info.user.permissions:
            return [get_pipeline, update_forecast]
        return [get_pipeline]
    return []

SkillsMiddleware(backend=backend, sources=["/skills/"], tools=resolve_skill_tools)

更多内容(例如将一个名称映射到 MCP 服务器上的所有工具)请参见向技能添加工具。

固定技能

有时用户已经知道自己想要哪个技能。在我们的 GTM 智能体中,销售代表可以输入 /meeting-prep 来为明天的 Acme 通话做准备。如果不固定,模型只能看到技能的描述,必须去读取它。这会在工作开始前增加一次往返,而且模型并不保证会加载正确的技能。使用固定技能时,你的应用会在消息中找到技能名称(或从 UI 中解析),并将它们传入 pinned_skills,中间件会在下一次模型调用前将每个技能的指令添加到对话中。Deep Agents 不会自行解析消息,因此语法由你决定:

输入 /meeting-prep 会指定该技能,因此应用可以将其固定,供智能体下一次模型调用使用。
输入 /meeting-prep 会指定该技能,因此应用可以将其固定,供智能体下一次模型调用使用。

输入 /meeting-prep 即指定了该技能,因此应用可以为智能体的下一次模型调用固定它。

message = "/meeting-prep for my Acme call tomorrow"
pinned = re.findall(r"(?<!\S)/([a-z0-9-]+)", message)  # ["meeting-prep"]

agent.invoke(
    {"messages": [{"role": "user", "content": message}], "pinned_skills": pinned},
    config=config,
)
被固定技能的指令已在对话中,因此智能体在模型调用 1 就开始工作,而不是模型调用 2。
被固定技能的指令已在对话中,因此智能体在模型调用 1 就开始工作,而不是模型调用 2。

固定技能的指令已经在对话中,因此智能体在模型调用 1 时就开始工作,而不是等到模型调用 2。

这缩短了延迟,也让行为更可预测:指令保证在上下文中,固定技能绑定的工具也随之而来。每个固定技能都作为一条带标签的消息添加一次,因此更早的消息永远不会改变,提示缓存保持有效,聊天界面可以将该技能显示为一个标签,而不是它的全文。

在线程中途重新加载技能

技能在每个线程开始时加载,并保存在智能体状态中,因此之后的每一轮都会复用同一组技能。现在,你可以在调用智能体时将 skills_metadata 设为 None 来使这个列表失效。如果队友向库中添加了一个 competitive-intel-card 技能,应用程序可以选择使技能列表失效,下一次运行将重新扫描每个来源:

agent.invoke(
    {"messages": messages, "skills_metadata": None},
    config=config,
)
将 skills_metadata 设为 None 会使下一次运行重新扫描技能库,并拾取自上次运行以来新增的技能。
将 skills_metadata 设为 None 会使下一次运行重新扫描技能库,并拾取自上次运行以来新增的技能。

将 skills_metadata 设为 None 会让下一次运行重新扫描技能库,并拾取自上次运行以来新增的技能。

重新加载并发现新技能会改变系统提示词,从而使提示词缓存失效。对于闲置已久的线程,这一成本通常已经付过了:提供商的缓存通常在闲置几分钟到一小时后过期(Anthropic、OpenAI),所以当代表回来时,缓存已经冷了。

由于重置只是运行输入,你也可以把控制权交给用户。例如,客户端上的一个 /reload 命令:

payload = {"messages": [{"role": "user", "content": message}]}
if message.startswith("/reload"):
    payload["skills_metadata"] = None

agent.invoke(payload, config=config)

你也可以从 update_state 或中间件进行重置,这样你的应用就能控制技能重新加载的时机。参见重新加载技能。

开始使用

技能是为智能体提供有组织的领域知识的行业标准机制。这些更新让技能更容易大规模运行:工具仅在技能需要时才加载,工作流所需的技能会预先加载,长时间运行的线程会随着你的技能库变化而保持最新。而且由于技能是开放标准,你的团队编写的技能可跨模型和智能体使用。

所有这些都已在最新的 deepagents 中提供。阅读技能文档开始使用,并通过 GitHub issues、论坛或 X 告诉我们你的想法。

致谢

感谢 Rich Scarrott 主导开发这些新功能,感谢 Hunter Lovell 对功能及博客的审阅!

已读完 · 本文由熊猫易读翻译重排