Skill
Agent Skills 把可复用的任务封装为“技能目录”,用 SKILL.md
描述目标与流程,并配套脚本与文档。在对话中,Agent 只注入
“低成本的概览”,在确有需要时再按需载入正文与文档,并在
隔离工作区中安全执行脚本,从而降低上下文占用与泄漏风险。
参考背景: - Anthropic 工程博客: https://www.anthropic.com/engineering/equipping-agents-for-the-real-world-with-agent-skills - 开源 Skills 示例库(结构与约定可借鉴): https://github.com/anthropics/skills
概览
🎯 能力一览
- 🔎 自动注入技能“概览”(名称与描述),引导模型选择
- 📥
skill_load按需注入SKILL.md正文与选定文档 - 📚
skill_select_docs增/改/清除文档选择 - 🧾
skill_list_docs列出可用文档 - 🧪 执行路径:
skill_load会在/skills/<name>/下物化出可写的 技能工作副本,脚本通过workspace_exec执行(只要配置了 code executor 即可) - 🗂️ 按通配符收集输出文件并回传内容与 MIME 类型
- 🧩 可选择本地或容器工作区执行器(默认本地)
核心概念:三层信息模型
1) 初始“概览”层(极低成本)
- 仅注入 SKILL.md 的 name 与 description 到系统消息。
- 让模型知道“有哪些技能、各做什么”,但不占用正文篇幅。
2) 正文层(按需注入)
- 当任务确实需要某技能时,模型调用 skill_load,框架把该
技能的 SKILL.md 正文物化到下一次模型请求中(详见下文
Prompt Cache 小节)。
3) 文档/脚本层(精确选择 + 隔离执行)
- 关联文档按需选择(通过 skill_load 或 skill_select_docs),
仅把文本内容物化到提示词;脚本不会被内联,而是在工作区中
执行,并回传结果与输出文件。
Token 成本
如果把一个技能仓库的全部内容(所有 SKILL.md 正文与 docs)
一股脑塞进提示词,往往会让 prompt token 占用变得非常高,甚至
直接超过模型上下文窗口。
想要可复现、基于真实运行的 token 对比(渐进披露 vs 全量注入),
可参考 trpc-agent-go-benchmark/anthropic_skills/README.md,并按其中说明运行
token-report 套件。
Prompt Cache
一些模型服务支持 prompt cache:如果后续一次模型请求的开头 (token 前缀)与之前某次请求完全一致,服务端可以复用这段共同 前缀,从而减少计算,并降低延迟和/或输入 token 成本(取决于服务商)。
对于 Skills,“已加载的 SKILL.md / docs”落在消息序列的哪里,会影响
连续模型调用之间可复用的前缀长度:
- 旧行为(默认):把已加载内容追加到 system message。
- 这会在 user/history 之前插入新 token,导致连续模型调用的共同前缀 变短。
- Tool-result 物化(可选):把已加载内容追加到对应的 tool result
消息(
skill_load/skill_select_docs)。- system message 更稳定,早期消息更不容易“后移”,prompt cache 往往能 命中更多前缀 token。
回退机制:如果对应的 tool result 消息不在本次请求的 history 里 (例如启用了 history suppression),框架可以回退为插入一条专用的 system message,确保模型仍能看到已加载内容。
Session summary 提醒:如果你启用了会话摘要注入
(WithAddSessionSummary(true)),并且本次请求里确实插入了摘要,
框架会尽量跳过这条回退 system message,避免把“已被 summary 掉的
内容”又塞回提示词里。如果对应的 tool result 仍在提示词里,回退会继续
保持关闭;如果 same-turn summary compaction 已经把这些 tool result
裁掉,回退会重新开启,保证模型仍能看到完整正文/文档。
启用方式:llmagent.WithSkillsLoadedContentInToolResults(true)。
如果你希望在 summary 场景恢复旧的回退行为:
llmagent.WithSkipSkillsFallbackOnSessionSummary(false)。
要在真实工具链路中测量提升,参见 trpc-agent-go-benchmark/anthropic_skills 的
prompt-cache 套件。
与 SkillLoadMode 的关系(容易踩坑):
- 上面讨论的“缓存前缀变短/变长”,主要发生在同一次
Runner.Run里多次调用模型的场景(一次用户消息触发多个 tool call)。 - 如果你希望跨多轮对话继续复用“已加载技能的正文/文档”,需要把
SkillLoadMode设为session。默认turn会在下一轮开始前清空 该 agent 的 skill state key(temp:skill:loaded_by_agent:<agent>/<name>/temp:skill:docs_by_agent:<agent>/<name>),因此即使 history 里仍然 有上一轮的skill_loadtool result(通常是loaded: <name>这种短 stub),框架也不会再把正文/文档物化进去。
实践建议(尤其是 WithSkillsLoadedContentInToolResults(true) 时):
- 先确认你在讨论哪种“缓存”场景:
- 同一轮对话内(一次
Runner.Run里多次调用模型):turn与session基本等价,因为它们都会让“本轮已加载内容”在该轮内可见。 更关键的开关通常是“注入到 system 还是 tool result”。 - 跨多轮对话:
session可能更利于 prompt cache,因为你只需加载一次, 后续不必反复skill_load;但代价是上下文更大、需要更主动地管理清理。
- 同一轮对话内(一次
- 经验法则:
- 默认用
turn(最小权限、上下文更小、也更不容易触发截断/summary)。 - 仅对“整段会话都会反复用到”的少量技能用
session,并严格控制 docs。
- 默认用
- 严格控制 docs 选择(尽量不要
include_all_docs=true),否则很容易把 上下文塞爆,进而触发 history 截断/summary,导致回退为 system message, prompt cache 的收益会下降。
会话持久化
先区分两个概念:
- Session(持久化):保存事件流(用户消息、助手消息、工具调用/结果)
- 一份小的键值 state map。
- 模型请求(一次性的):本次发给模型的
[]Message,由 Session + 运行时配置拼出来。
skill_load 只会把“已加载/已选文档”的小状态写入 Session(例如
temp:skill:loaded_by_agent:<agent>/<name>、
temp:skill:docs_by_agent:<agent>/<name>;旧版 key 也仍被支持)。
随后由请求处理器在
下一次模型请求里,把对应的 SKILL.md 正文/已选 docs 物化
进去。
重要:物化不会把“扩展后的 tool result 内容”写回 Session。
所以如果你去看 Session 里保存的工具结果,skill_load 仍然通常是
一个很短的 stub(比如 loaded: internal-comms)。但模型在每次请求
里仍能看到完整正文/文档,因为它们是在构造请求时注入的。
补充:SkillLoadMode 控制的是这些 state key 的生命周期,所以也决定了
“下一轮对话”里是否还能继续物化正文/文档。
后续请求的稳定性:
- 在同一次工具链路里,每次模型调用前都会按同一套规则重新物化,
所以只要 skills 仓库内容和选择状态不变,模型看到的 skill 内容
就是稳定的。
- 如果本次请求的 history 里找不到对应的 skill_load /
skill_select_docs tool result(常见原因:history suppression、
会话摘要、或截断把这些 tool 消息移除),框架可以回退为插入一条专用
system message(Loaded skill context:),把缺失的 skill 正文/文档
补回来,确保模型仍能看到正确上下文。
- 但这会改变 system 内容,prompt cache 的收益可能变小;因此当本次请求
存在 session summary 时,该回退默认会被跳过(见上文)。
与业界实现对比
很多框架为了更友好地利用 prompt cache,会尽量避免在多步工具链路中 不断改写 system prompt,而是把动态上下文放到 tool 消息(工具结果) 里,让 system 更稳定。
一些例子:
- OpenClaw:system prompt 列出可用 skills,但选中 skill 的 SKILL.md
会要求通过工具读取(正文落在 tool result 里):
https://github.com/openclaw/openclaw/blob/0cf93b8fa74566258131f9e8ca30f313aac89d26/src/agents/system-prompt.ts
- OpenAI Codex:项目文档中渲染 skills 列表,并要求按需打开 SKILL.md
(正文来自读文件工具的 tool result):
https://github.com/openai/codex/blob/383b45279efda1ef611a4aa286621815fe656b8a/codex-rs/core/src/project_doc.rs
在 trpc-agent-go 中:
- 旧模式:把已加载的 skill 正文/文档追加到 system message
(简单、兼容旧语义,但可能缩短可缓存的前缀)。
- 新模式(可选):保持 system 更稳定,把已加载内容物化到 skill_load /
skill_select_docs 的 tool result 消息中(更接近“工具消息承载动态上下文”
的主流模式)。
目录结构
仓库与解析: skill/repository.go
快速开始
1) 环境准备
- Go 1.21+
- 一个模型服务的 API Key(OpenAI 兼容)
- 可选:Docker(使用容器执行器时)
常用环境变量:
2) 启用 Skills
在 LLMAgent 里提供技能仓库即可。
NewFSRepository 也可以同时扫描多个根目录,常见做法是把通用
skills 目录和用户私有 skills 目录一起传入:
如果是一个常驻 Agent 服务复用同一个 skills 仓库来处理多种请求,
可以再加一层按请求生效的可见性过滤。过滤函数可以从 ctx /
运行时状态里读取任意业务信号(例如 user_id、tenant_id、角色、
实验开关等),并把不匹配的 skill 从概览、工具声明和运行时校验里
一起隐藏。下面用 user_id 只是举例:
如果你的进程在启动后还会安装、删除或重命名 skill,请在文件系统
变更完成后调用一次 repo.Refresh(),让下一轮请求看到最新技能
集合。Refresh() 适用于仓库结构变化,不建议每次请求前都调用。
细粒度白名单(只暴露知识注入类工具,适合只读型代理):
要点:
- 请求处理器注入概览与按需内容:
[internal/flow/processor/skills.go]
(https://github.com/trpc-group/trpc-agent-go/blob/main/internal/flow/processor/skills.go)
- WithSkills 会自动注册内置 skill 工具(skill_load、
skill_select_docs、skill_list_docs),无需手动添加。它们都属于
“知识注入”类工具,不直接执行脚本。
- WithAllowedSkillTools(...) 可以用显式白名单进一步收窄这套工具集,
例如只保留 SkillToolLoad。
- 执行器自动 fallback:当 WithSkills(repo) 开启但没有显式传
WithCodeExecutor(...) 时,框架会自动挂一个本地 code executor,
让 workspace_exec 开箱可用。模型随后通过 workspace_exec 在
/skills/<name>/ 的可写工作副本里按 SKILL.md 描述的步骤执行脚本
并收集输出文件。以下三种情况会跳过 fallback:
(1)你已经传了 WithCodeExecutor(...)(用你自己的 executor);
(2)你用了 WithAllowedSkillTools(...) 做精细控制(不做魔法);
(3)你显式传了 WithSkillToolProfile(SkillToolProfileKnowledgeOnly)
("我不要框架帮我起执行器"的 opt-out 信号)。生产环境建议显式
配置容器 executor,而不是依赖本地 fallback —— 本地 fallback 只是
开发期便利,不是上线目标形态。
- CodeExecutor 与围栏代码自动执行是两个独立开关:
配置 CodeExecutor 只是让执行类工具(例如 workspace_exec)
可用,本身并不会让框架扫描模型回复里的 Markdown 围栏代码块并直接
运行。后者由独立的 EnableCodeExecutionResponseProcessor 控制
(默认:true)。两条推论:
- 当你显式 WithCodeExecutor(...) 时,围栏代码自动执行保持框架
默认值,不会被 skills 逻辑偷偷改动。如果你只想让 executor 服务
workspace_exec,请显式传
llmagent.WithEnableCodeExecutionResponseProcessor(false)。
- 当 skills fallback 代你注入本地 executor 时(即上面的
WithSkills(repo) 场景),该隐式 executor 的用途被严格收敛为
"给 workspace_exec 供电":如果你没有显式调用过
WithEnableCodeExecutionResponseProcessor(...),fallback 路径
会自动把 EnableCodeExecutionResponseProcessor 置为 false,
避免 WithSkills(repo) 的升级路径偷偷打开你原本没配过的围栏
代码自动执行能力。
- 默认提示指引:框架会在系统消息里,在 Available skills: 列表后
追加一段 Tooling and workspace guidance: 指引文本。
- 关闭该指引(减少提示词占用):
llmagent.WithSkillsToolingGuidance("")。
- 或用自定义文本替换:llmagent.WithSkillsToolingGuidance("...")。
- 指引会跟随最终注册的 skill 工具集,包括
WithAllowedSkillTools(...)。
- 如果你关闭它,请在自己的指令里明确当前白名单下哪些 skill 工具
可用。
- 加载器: tool/skill/load.go
3) 运行示例
GAIA 基准示例(技能 + 文件工具 + workspace_exec):
examples/skill/README.md
该示例包含数据集下载脚本,以及 whisper(音频)/ocr(图片)等
技能的 Python 依赖准备说明。
真实技能发现/安装示例(真实模型 + 真实公网/GitHub): examples/skillfind/README.md
这个示例从内置的 skill-find skill 出发,先到公网搜索候选
skills,再把 GitHub 上的公开 skill 安装到用户私有目录中,调用
repo.Refresh() 让仓库立即重新发现,然后在同一个会话里继续使用
新 skill。
SkillLoadMode 演示(无需 API key): examples/skillloadmode/README.md
子代理 skill 隔离演示(AgentTool + Skills): examples/skillisolation/README.md
快速开始(下载数据集 JSON 到 examples/skill/data/):
如需同时下载引用到的附件文件:
自然语言交互建议:
- 直接说明你要做什么;模型会根据概览判断是否需要某个技能。
- 当需要时,模型会先调用 skill_load 注入正文/文档,然后按
SKILL.md 里描述的步骤通过 workspace_exec 在 /skills/<name>/
下执行脚本并收集输出文件。
SKILL.md 结构与示例
SKILL.md 采用 YAML 头信息 + Markdown 正文:
建议:
- 头信息的 name/description 要简洁,便于“概览注入”
- 正文给出“使用时机”“步骤/命令”“输出文件位置”等
- 把脚本放入 scripts/,命令中引用脚本路径而非内联源码
更多可参考 Anthropic 的开源库: https://github.com/anthropics/skills
工具用法详解
skill_load
输入:
- skill(必填):技能名
- docs(可选):要包含的文档文件名数组
- include_all_docs(可选):为 true 时包含所有文档
行为:
- 写入会话临时键(按 agent 隔离,生命周期由 SkillLoadMode 控制):
- temp:skill:loaded_by_agent:<agent>/<name> = "1"
- temp:skill:docs_by_agent:<agent>/<name> = "*" 或 JSON 字符串数组
- temp:skill:loaded_order_by_agent:<agent> = JSON 字符串数组,
按“最早触达 -> 最新触达”的顺序保存 skill 名
- 旧版 key(temp:skill:loaded:<name>、temp:skill:docs:<name>)仍被支持,
并在读到时自动迁移。
- 多代理提示:transfer 调用子代理时通常共享同一个 Session。key 按 agent
隔离后,子代理的 skill_load 不会自动把技能正文/文档“塞进”主代理的提示词。
如果主代理确实需要该技能正文/文档,请让主代理自己调用一次 skill_load。
- 请求处理器读取这些键,把 SKILL.md 正文与文档物化到下一次模型请求中:
- 默认:追加到系统消息(兼容旧行为)
- 可选:追加到对应 tool result 消息
(llmagent.WithSkillsLoadedContentInToolResults(true))
在每次模型请求前获取“已加载技能列表”
如果你希望在每次模型请求前拿到当前 llmagent 已加载的 skill 列表
(包括一次 Runner.Run 内的 tool loop 里每一步),推荐使用
ModelCallbacks 的 BeforeModel,并通过 context 取出当前
Invocation。
从最基本的机制出发理解:
skill_load并不会把完整的SKILL.md正文写进 session 的事件记录里。- 它只会往 session state 写入一些很小的“标记键”(按 agent 隔离):
- Loaded 标记:前缀为
skill.LoadedPrefix(inv.AgentName)的 key - Docs 选择:前缀为
skill.DocsPrefix(inv.AgentName)的 key
- Loaded 标记:前缀为
- Skills 的请求处理器会读取这些键,把正文/文档物化到下一次 outbound 模型请求里。
因此,如果你只是想拿到“当前哪些 skills 已加载”,只需要读 session state 即可:
下面的代码片段中,m 表示你的模型实例,repo 表示 skills 仓库。
注意:
- 默认
SkillLoadModeTurn会在下一轮Runner.Run开始前清空这些 该 agent 的temp:skill:loaded_by_agent:*/temp:skill:docs_by_agent:*/temp:skill:loaded_order_by_agent:*state key,所以“已加载列表”通常只在当前这轮 tool loop 里非空。 SkillLoadModeSession会跨轮保留这些 key,所以“已加载列表”会一直 非空,直到你手动清空(或会话过期)。
内置选项:限制已加载技能数量(TopK)
如果你的需求只是“最多保留最近 N 个已加载 skills”,可以直接使用内置 option:
llmagent.WithMaxLoadedSkills(N)
它会在每次模型请求前检查当前 loaded skills,并清空更老的
temp:skill:* state key。最近 skill 的触达顺序会写入 session state,
并由 skill_load / skill_select_docs 自动更新,因此不会依赖字母序兜底
或 tool result history 是否还完整保留。
示例:
自定义策略:限制已加载技能数量(比如最多保留最近 3 个)
SkillLoadMode 解决的是“驻留多久”(once/turn/session),不解决
“最多加载多少个”。如果你需要在 llmagent.WithMaxLoadedSkills 之外
做更细粒度的手动控制(比如自定义淘汰策略),推荐在 session service
上加 AppendEventHook,去修改 skill_load 写入的 state delta。
核心思路:
1) 识别“加载 skill”的事件(state delta 里包含
该 agent 的 loaded key,即 key 前缀为 skill.LoadedPrefix(ev.Author))。
2) 计算“应用该 delta 后”会有哪些 skills 处于 loaded 状态。
3) 如果数量超过阈值,在同一个 StateDelta 里把要淘汰的 skills 对应
key 置为 nil(清空)。
示例(inmemory session service,最多保留最近 3 个 loaded skills):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 | |
AppendEventHook 的接口说明见 Session 文档,也可以参考
可运行示例 examples/session/hook。
说明:
- 建议采用“渐进式披露”:默认只传 skill 加载正文;需要文档时先
skill_list_docs 再 skill_select_docs,只选必要文档;除非确
实需要全部(或用户明确要求),避免 include_all_docs=true。
- 可多次调用以新增或替换文档。
- 工具会写入 session state,但正文/文档在提示词里驻留多久取决
于 SkillLoadMode:
- turn(默认):在当前一次 Runner.Run(处理一条用户消息)
的所有模型请求中驻留;下一次运行开始前自动清空。
- once:只在下一次模型请求中注入一次,随后自动 offload
并清空对应 state。
- session(兼容旧行为):跨多轮对话保留,直到手动清除或会话过期。
- 常见疑问:为什么你在 tool result 里只看到 loaded: <name>,没看到
[Loaded] <name> + 正文?
- 先确认你是否开启了 tool-result 物化:
llmagent.WithSkillsLoadedContentInToolResults(true)。
未开启时,正文/文档会被追加到 system message,而不是 tool result。
- 如果已开启,但你看到“第二轮对话”的请求里仍只有 stub,通常是因为
你使用了默认的 SkillLoadModeTurn:下一轮开始前框架会清空 state,
于是不会再物化正文/文档。需要跨轮保留时,改用:
多轮对话示例:复用同一个 sessionID 才能让“已加载状态”跨轮生效:
清空建议(SkillLoadModeSession 下很常见):
- 最简单:换一个新的
sessionID开启新对话。 - 或者由上层删除 session(以 inmemory 为例):
提示:tool-result 物化依赖本次请求的 history 中包含对应的 tool result
消息;如果 history 被截断/抑制,框架会回退为插入专用 system message
(Loaded skill context:)来保证正确性。
- 在 agent 上配置:
配置片段:更利于 prompt cache 的常用组合(system 更稳定 + 只在本轮驻留):
配置片段:一次性注入(只让下一次模型请求看到正文/文档):
skill_select_docs
输入:
- skill(必填)
- docs(可选数组)
- include_all_docs(可选布尔)
- mode(可选字符串):add | replace | clear
行为:
- 更新当前 agent 的 doc 选择 state key:
- temp:skill:docs_by_agent:<agent>/<name>:* 表示全选;数组表示显式列表
- 同时刷新 temp:skill:loaded_order_by_agent:<agent>,因此
WithMaxLoadedSkills(N) 会把文档选择也视作一次“最近触达”
- 旧版 key temp:skill:docs:<name> 仍被支持,并在读到时自动迁移
skill_list_docs
输入:
- skill(必填)
输出: - 可用文档文件名数组
提示:这些会话键由框架自动管理;用户通常无需直接操作,仅需用 自然语言驱动对话即可。
技能工作区与运行时环境
当模型通过 workspace_exec 执行 skill 脚本时,框架会为每个会话在
工作区里物化一份可写的技能副本,并注入一组便于脚本使用的环境变量
与符号链接:
- 技能根目录
/skills/<name>默认是会话级的可写工作副本:脚本 可以在源码旁创建缓存、__pycache__、.venv/等文件。上游 技能仓库仍是事实来源;当源摘要发生变化时,下次 reconcile 会 替换工作区中的副本。 - 便捷符号链接:在技能根目录下自动创建
out/、work/、inputs/链接到工作区对应目录,方便按文档中的相对路径使用。 - 注入环境变量:
WORKSPACE_DIR、SKILLS_DIR、WORK_DIR、OUTPUT_DIR、RUN_DIR(由执行器注入)。 .venv/:技能根目录下的可写目录,用于安装技能依赖 (例如python -m venv .venv+pip install ...)。- 文件工具在 base directory 下不存在真实
inputs/目录时,会把inputs/<path>视为<path>的别名。
执行器
接口: codeexecutor/codeexecutor.go
实现: - 本地: [codeexecutor/local/workspace_runtime.go] (https://github.com/trpc-group/trpc-agent-go/blob/main/codeexecutor/local/workspace_runtime.go) - 容器(Docker): [codeexecutor/container/workspace_runtime.go] (https://github.com/trpc-group/trpc-agent-go/blob/main/codeexecutor/container/workspace_runtime.go)
容器模式说明:
- 运行目录挂载为可写;$SKILLS_ROOT(若存在)只读挂载
- 默认禁用网络(参见容器 HostConfig),更安全可重复
安全与资源:
- 本地/容器均限制读取与写入在工作区内
- /skills/<name> 工作副本默认可写;canonical 技能仓库仍是
唯一事实来源
- stdout/stderr 可能会被截断(见 warnings)
- 输出文件读取大小有限制,避免过大文件影响
事件与追踪
事件:工具响应以 tool.response 形式产出,可携带状态增量(见
skill_load)。合并多工具结果与并行执行逻辑参见:
[internal/flow/processor/functioncall.go]
(https://github.com/trpc-group/trpc-agent-go/blob/main/internal/flow/processor/functioncall.go)
追踪(常见 span 名):
- workspace.create、workspace.stage.*、workspace.run
- workspace.collect、workspace.cleanup、workspace.inline
原理与设计
- 动机:在真实任务中,技能说明与脚本往往内容较多,全部内联到 提示词既昂贵又易泄漏。三层信息模型让“知道有何能力”与“在 需要时获得细节/执行脚本”解耦,从而减少上下文开销并提升安全。
- 注入与状态:通过事件中的
StateDelta将加载选择以键值形式 写入会话状态的temp:*命名空间,后续每轮请求处理器据此拼接 提示词上下文(默认拼接系统消息;也可按需物化到 tool result), 形成“概览 → 正文/文档”的渐进式上下文。 - 执行隔离:脚本以工作区为边界,输出文件由通配符精确收集,避免 将脚本源码或非必要文件带入模型上下文。
执行器环境变量注入
当执行器运行在远端(容器、云函数等)时,宿主进程的环境变量不会
自动传递。codeexecutor.NewEnvInjectingCodeExecutor 可以包装
任意 CodeExecutor,在每次 RunProgram / StartProgram
调用前,从 context 动态读取环境变量并合并到
RunProgramSpec.Env。
行为:
- 覆盖所有走
Engine.Runner()的执行路径(例如workspace_exec)。 - provider 返回的 key 不覆盖 tool 显式传入的
env。 - 每次
RunProgram调用时求值,不在调用间共享状态。 - provider 返回
nil时零开销跳过。 - 也可以只包装 Engine 层:
codeexecutor.NewEnvInjectingEngine(eng, provider)。
典型场景:多用户 Agent 服务中,每个用户通过 AG-UI state 或
HTTP header 传入自己的 token,provider 从请求上下文中读取后注入
执行器,LLM 无需感知。
故障排查
- “unknown skill”:确认技能名与仓库路径;调用
skill_load前 先检查“概览注入”是否包含该技能 - 没配 executor:默认情况下,
WithSkills(repo)会自动 fallback 到 一个本地 executor。如果你显式 opt out 了 (WithSkillToolProfile(SkillToolProfileKnowledgeOnly)或WithAllowedSkillTools(...)),又想要workspace_exec,就显式传llmagent.WithCodeExecutor(...)(本地或容器)。 - 超时/非零退出码:检查命令、依赖与超时参数;容器模式下网络默认关闭, 避免依赖网络的脚本
- 输出文件未收集到:确认
workspace_exec使用的通配符是否指向工作区 内的实际路径
参考与示例
- 背景:
- 工程博客: https://www.anthropic.com/engineering/equipping-agents-for-the-real-world-with-agent-skills
- 开源库:https://github.com/anthropics/skills
- 业界实践:
- OpenClaw:在 prompt 中要求模型用工具读取所选 skill 的
SKILL.md: https://github.com/openclaw/openclaw/blob/0cf93b8fa74566258131f9e8ca30f313aac89d26/src/agents/system-prompt.ts - OpenAI Codex:在项目文档里列出 skills,并要求按需打开
SKILL.md: https://github.com/openai/codex/blob/383b45279efda1ef611a4aa286621815fe656b8a/codex-rs/core/src/project_doc.rs
- OpenClaw:在 prompt 中要求模型用工具读取所选 skill 的
- 本仓库:
- GAIA 基准示例: [examples/skill/README.md] (https://github.com/trpc-group/trpc-agent-go/blob/main/examples/skill/README.md)
- 真实技能发现/安装示例: [examples/skillfind/README.md] (https://github.com/trpc-group/trpc-agent-go/blob/main/examples/skillfind/README.md)
- 子代理 skill 隔离示例: [examples/skillisolation/README.md] (https://github.com/trpc-group/trpc-agent-go/blob/main/examples/skillisolation/README.md)