Agent技能化:从函数调用到可复用技能库的工程实践

发布时间:2026/9/17 10:43:10
Agent技能化:从函数调用到可复用技能库的工程实践 agent-skills这个词最近在AI开发圈子里热度涨得很快。如果你跟我一样日常在跟大模型Agent打交道一定遇到过这种尴尬同一个agent项目里工具函数堆了一大堆prompt越写越长逻辑纠缠在一起改一个地方崩三处换个场景复用又得从零开始复制粘贴。agent-skills就是冲着这个痛点去的——它把“技能”做成一等公民用一套标准化的方式定义、注册、组合、复用agent的能力让复杂任务变得像搭积木一样清晰。这篇文章不是官方文档的翻译而是我自己在多个项目里把这一类思路落地之后踩过坑、翻过车、最后沉淀下来的实操总结。内容适合两类人一是已经写过几个agent demo、正在思考怎么工程化的人二是想给自己的agent加“可插拔技能”但还没找到干净方案的人。我会从概念拆解讲到具体实现再给出完整的代码示例和排错记录尽量让你读完就能在自己的项目里动手改起来。1. 项目整体设计与思路拆解1.1 为什么需要“技能”这个抽象层先聊一个本质问题大模型Agent的本质是让模型在“思考”和“行动”之间循环。思考靠prompt和上下文行动靠调用外部工具。刚开始做demo的时候你可以在system prompt里写“你可以使用以下工具”然后把函数定义一股脑塞进去。项目小的时候没问题但一旦工具超过五六个、业务流程超过两轮这种写法立刻失控。失控体现在几个层面。第一是上下文污染每个工具的长描述、参数说明、使用限制全都挤在prompt里模型注意力被稀释关键指令反而容易被忽略。第二是耦合严重工具之间的调用顺序、数据格式传递、错误处理散落在主逻辑里改一个工具其他逻辑跟着遭殃。第三是复用困难你在项目A里写好的“搜索总结”流程换到项目B里根本搬不过去因为它是跟A的prompt和状态管理长在一起的。agent-skills解决的核心问题就是把“能力”和“流程”解耦。一个skill不是简单的函数而是一个自包含的模块——它包含触发条件、运行逻辑、输入输出schema、依赖的其他技能、以及对应的prompt片段。Agent的主循环只需要做一件事根据当前目标从技能库里选出合适的技能然后执行它。这样设计之后新增能力就是塞一个新的skill文件夹复用能力就是加载一个现成skill包排查问题就是把每个skill当成一个独立的黑盒来测。1.2 与函数调用function calling的区别和关系很多人会问OpenAI的function calling已经很成熟了为什么还要自己做一套skill层我的理解是function calling是“单个动作”的接口规范而skill是“一组动作”的流程封装。打个比方function calling好比给工人提供了一把电钻而skill是一个“打孔埋膨胀螺丝挂画框”的完整工序。举一个具体场景你让agent“帮我把这篇网页文章整理成摘要发到我的笔记里”。如果裸用function calling你需要写fetch网页、格式转换、调用摘要模型、写笔记四个函数然后在主逻辑里编排它们的调用顺序还要处理中间数据。而如果用skill设计你只需要一个“web_to_note”的skill内部自己管理步骤外部只暴露两个参数URL和目标笔记ID。模型只需要做一次决策——“这个任务匹配web_to_note技能”——剩下的步骤全部由skill内部消化。当然这两者不是对立关系。现代Agent框架里skill的内部实现完全可以依赖function calling或tool calling。关键在于分层底层是细粒度的function上层是粗粒度的skill模型先选skill再让skill内部调度function。这样既保留了function calling的结构化输出优势又获得了业务流程的可编排性。1.3 技能库应该具备哪些核心能力经过几次重构我认为一个合格的agent-skills项目至少得满足六个条件缺一个后期都会还债标准化的技能定义每个skill有统一的元信息名称、描述、版本、作者、输入schema、输出schema、依赖声明。独立可测试每个skill可以脱离agent主流程单独跑通输入确定输出可预期。动态注册与发现系统能扫描技能目录自动加载新技能不用改主程序代码。依赖管理技能之间可以互相调用但依赖关系要显式声明避免隐式引用导致的神秘错误。上下文管理技能执行过程中的中间数据要可控不能把无关上下文丢回主对话里。可观测性技能调用的开始、结束、耗时、成功失败要有日志方便定位问题。这些能力听起来多其实实现起来并不复杂。核心思路就是约定大于配置定义好目录结构、文件格式、接口签名然后写一个轻量级的注册器。下面两章我会从零开始搭一个最小的可运行版本。2. 核心细节解析与实操要点2.1 技能目录与元信息设计我在实践中采用的目录结构是这样的agent-skills/ ├── skills/ │ ├── web_search/ │ │ ├── skill.yaml │ │ ├── run.py │ │ ├── prompt.md │ │ └── requirements.txt │ ├── summarize/ │ │ ├── skill.yaml │ │ ├── run.py │ │ └── prompt.md │ └── weather_lookup/ │ ├── skill.yaml │ ├── run.py │ └── requirements.txt ├── core/ │ ├── registry.py │ ├── executor.py │ └── schema.py └── main.py每个技能文件夹就是一个独立的技能包。skill.yaml是核心元信息文件下面是一个实例name: web_search version: 1.2.0 description: 通过搜索引擎获取网页结果并返回标题、链接和摘要列表。 author: alice tags: [search, web] input_schema: type: object properties: query: type: string description: 搜索关键词。 max_results: type: integer description: 返回结果数量默认5。 default: 5 required: [query] output_schema: type: array items: type: object properties: title: { type: string } url: { type: string } snippet: { type: string } dependencies: - http_client这里有几个容易忽略的细节。description字段非常重要因为它是模型判断“该不该用这个技能”的依据。写得太泛比如“搜索”模型可能该用的时候不用写得太啰嗦又会占用token。我一般控制在50到100个汉字包含触发场景关键词和与相似技能的区分点。例如如果同时有“web_search”和“internal_search”我会在description里写明“适用于查询公开互联网信息不适用于公司内部文档”降低误选率。依赖声明dependencies解决的是跨技能复用问题。比如一个“写完笔记自动搜索相关图片”的技能依赖note_writer和image_finder两个技能。注册器在加载时会做拓扑排序保证先加载依赖项。2.2 技能执行接口的标准签名所有技能的执行入口我都统一用Python函数形式签名保持完全一致async def run(context: SkillContext, **kwargs) - SkillResult: ...SkillContext是环境上下文包含logger、模型客户端、HTTP会话、配置项等全局对象。kwargs是经过schema校验后的参数。SkillResult是统一返回结构包含success、data、error、duration等字段。之所以用async def是因为技能里大概率会发网络请求或调用模型异步是基本需求。统一签名带来的最大好处是executor可以用完全一样的代码去调度所有技能不需要针对某个技能写特例。热加载新技能时只需要把新技能的run函数注册进去其余全部复用。在SkillContext里我还会放一个step_notify回调函数用来向主流程汇报技能内部的关键进度。这个设计是之后做可观测性的基础。你可以在run.py里执行长耗时任务时每完成一个子步骤就调用一次step_notify这样即使技能最后失败你也能知道它卡在了哪一步。2.3 Prompt片段如何跟随技能走很多人把技能里的prompt写在主system prompt里这恰恰是导致混乱的最大原因。正确的做法是让每个技能自带prompt片段在执行时才注入。我的做法是在skill文件夹放一个prompt.md里面写的是该技能在与主模型交互时的系统级指令。举例summarize技能的prompt.md你是一个内容摘要专家。你将收到一段文本请输出结构化摘要。 要求 1. 用不超过200字概括核心观点。 2. 分点列出关键论据每点的第一个词必须是动词。 3. 最后输出“可引用的原句”最多三条必须加引号。 4. 如果文本包含数据结论必须注明数据来源。这个prompt片段在该技能被选中执行时以独立消息块的形式注入而不是预先塞进主prompt。好处很明显主prompt始终精简模型注意力更聚焦。其次prompt跟代码放在同一个技能目录改摘要风格不需要动主程序直接改md文件即可对非程序员也很友好。一个需要特别注意的地方模型调用时的角色分配。技能里的prompt片段不是单纯拼进user消息而是作为system级别的指示或者作为单独的“技能指令”标识。我在实现中是把主system prompt和技能prompt拼接后用两个消息块传给模型中间用“【当前技能summarize】”这样的天然语言分隔实测比单块拼接效果好很多。2.4 技能的输入校验与容错skill.yaml里的input_schema不是摆设它应该被真正执行校验。我不建议自己在代码里手写一堆if判断而是直接用pydantic或jsonschema来做。在加载技能时根据yaml里的schema动态生成对应的校验器调用run之前强制校验。校验失败的错误信息要足够清晰。我见过最坑的错误提示是“invalid input”完全不知道缺了哪个字段。我在registry里会把校验错误翻译成这种格式技能 web_search 参数校验失败 - query: 该字段是必填项 - max_results: 值必须为整数这种提示既方便人看也方便模型下一次自我修正。容错方面我在SkillResult里专门留了retryable字段。对于超时、临时网络错误这类可重试的错误executor会按预设策略自动重跑一次。对于业务逻辑错误比如搜索结果为空则不重试直接返回避免浪费token和时间。3. 实操过程与核心环节实现3.1 最小可运行技能系统的完整代码下面我给出一个经过精简但可直接运行的实现。为控制篇幅这里用一个内存技能库和两个示例技能来演示重点展示注册、校验、执行的完整链路。先看core/schema.pyimport time import uuid from dataclasses import dataclass, field from typing import Any, Callable, Optional dataclass class SkillResult: success: bool data: Any None error: Optional[str] None retryable: bool False duration: float 0.0 trace_id: str field(default_factorylambda: str(uuid.uuid4())) def to_dict(self): return { success: self.success, data: self.data, error: self.error, retryable: self.retryable, duration: self.duration, trace_id: self.trace_id, } dataclass class SkillContext: logger: Any http_session: Any None llm_client: Any None step_notify: Optional[Callable[[str], None]] None config: dict field(default_factorydict)这个文件定义了最核心的数据结构。SkillResult是所有技能的统一出口trace_id用来关联日志。再看core/registry.pyimport asyncio import importlib import inspect from pathlib import Path from typing import Dict import yaml from pydantic import BaseModel, create_model class SkillDefinition: def __init__(self, meta: dict, module): self.name meta[name] self.description meta[description] self.version meta.get(version, 0.0.1) self.input_schema meta.get(input_schema, {}) self.output_schema meta.get(output_schema, {}) self.dependencies meta.get(dependencies, []) self.run_func module.run self.prompt_text self._load_prompt(module) def _load_prompt(self, module): prompt_path Path(module.__file__).parent / prompt.md if prompt_path.exists(): return prompt_path.read_text(encodingutf-8) return def build_input_validator(self): fields {} props self.input_schema.get(properties, {}) required set(self.input_schema.get(required, [])) for field_name, prop in props.items(): ptype prop.get(type, string) if ptype string: py_type (str, ...) elif ptype integer: py_type (int, ...) elif ptype boolean: py_type (bool, ...) elif ptype array: py_type (list, ...) else: py_type (Any, ...) if field_name not in required: default_val prop.get(default, None) py_type (Optional[py_type[0]], default_val) fields[field_name] py_type model create_model(f{self.name}_input, **fields) return model class SkillRegistry: def __init__(self): self._skills: Dict[str, SkillDefinition] {} def load_from_directory(self, skills_dir: str): skills_path Path(skills_dir) for skill_folder in skills_path.iterdir(): if not skill_folder.is_dir(): continue meta_file skill_folder / skill.yaml if not meta_file.exists(): continue meta yaml.safe_load(meta_file.read_text(encodingutf-8)) module importlib.import_module(fskills.{skill_folder.name}.run) skill_def SkillDefinition(meta, module) self._skills[skill_def.name] skill_def self._validate_dependencies() def _validate_dependencies(self): for name, skill in self._skills.items(): for dep in skill.dependencies: if dep not in self._skills: raise RuntimeError(f技能 {name} 依赖 {dep}但未找到该依赖) def get(self, name: str) - Optional[SkillDefinition]: return self._skills.get(name) def list_skills(self): return [ {name: s.name, description: s.description, version: s.version} for s in self._skills.values() ]这里有个小技巧用pydantic的create_model动态生成校验模型这样yaml里改schema代码不用跟着改。加载目录时直接importlib导入每个skill包里的run.py理所应当要求每个技能都是独立可导入的包。然后是core/executor.pyimport asyncio import time class SkillExecutor: def __init__(self, registry: SkillRegistry, context: SkillContext): self.registry registry self.context context async def execute(self, skill_name: str, params: dict) - SkillResult: skill_def self.registry.get(skill_name) if skill_def is None: return SkillResult(successFalse, errorf未找到技能: {skill_name}) validator skill_def.build_input_validator() try: validated validator(**params).model_dump() except Exception as e: return SkillResult(successFalse, errorf参数校验失败: {e}) start time.monotonic() try: result await skill_def.run_func(self.context, **validated) if isinstance(result, SkillResult): result.duration time.monotonic() - start return result return SkillResult(successTrue, dataresult, durationtime.monotonic() - start) except Exception as e: duration time.monotonic() - start return SkillResult(successFalse, errorstr(e), retryableTrue, durationduration)执行器的逻辑很直白取出技能定义校验参数调用run函数。这里我把所有异常统一捕获并转成SkillResult防止技能内部异常直接打穿主流程。写两个示例技能。第一个是web_search技能内部用fake数据代替真实请求skills/web_search/skill.yamlname: web_search version: 1.0.0 description: 执行互联网搜索获取网页标题、链接和摘要。适用于需要查询实时信息、最新资讯、事实核查等场景。 input_schema: type: object properties: query: type: string description: 搜索关键词。 max_results: type: integer description: 返回结果数量。 default: 5 required: [query]skills/web_search/run.pyimport asyncio async def run(context, query: str, max_results: int 5): context.logger.info(f[web_search] 查询: {query}) if context.step_notify: context.step_notify(正在发起搜索请求) await asyncio.sleep(0.1) # 模拟请求耗时 # 真实场景这里调用 requests/aiohttp 调用搜索引擎API fake_results [ {title: f{query} 相关结果 {i}, url: fhttps://example.com/{i}, snippet: f这是关于{query}的第{i}条摘要} for i in range(max_results) ] return fake_results第二个是summarize技能它不直接依赖外部搜索而是接收文本skills/summarize/run.pyasync def run(context, text: str, max_words: int 200): context.logger.info(f[summarize] 输入文本长度: {len(text)}) if context.step_notify: context.step_notify(正在生成摘要) # 真实场景这里调用LLM summary f模拟摘要核心观点{text[:50]}…… 共约{max_words}字。 return {summary: summary, keywords: [示例]}最后写main.py把所有东西串起来import asyncio import logging from core.registry import SkillRegistry from core.executor import SkillExecutor from core.schema import SkillContext logging.basicConfig(levellogging.INFO, format%(asctime)s %(name)s %(levelname)s %(message)s) logger logging.getLogger(agent-skills-demo) async def main(): registry SkillRegistry() registry.load_from_directory(skills) logger.info(已加载技能: %s, [s[name] for s in registry.list_skills()]) context SkillContext(loggerlogger, step_notifylambda msg: logger.info(f进度: {msg})) executor SkillExecutor(registry, context) # 执行搜索技能 result await executor.execute(web_search, {query: agent skills 最佳实践, max_results: 3}) print(搜索结果:, result.to_dict()) # 执行摘要技能 result2 await executor.execute(summarize, {text: 这是一段很长的文章内容讲的是Agent技能化的设计思路。}) print(摘要结果:, result2.to_dict()) if __name__ __main__: asyncio.run(main())运行main.py输出应该类似这样INFO 已加载技能: [web_search, summarize] INFO [web_search] 查询: agent skills 最佳实践 INFO 进度: 正在发起搜索请求 搜索结果: {success: True, data: [...], error: None, retryable: False, duration: 0.11, trace_id: ...}到这里一个最简技能系统已经跑通了。它具备注册、参数校验、统一调度、进度回调、错误封装这些核心能力后续加新技能只是加一个目录的事。3.2 与主Agent循环的集成方式上面这个小系统是纯调度器但真实场景里需要跟大模型推理循环集成。我的做法是在Agent的规划阶段把技能列表转成模型的function schema让模型决定调用哪个技能。关键点是这个function schema里对应的是skill级别而不是底层工具级别。例如将web_search技能转成如下function定义{ name: call_skill, parameters: { type: object, properties: { skill_name: { type: string, enum: [web_search, summarize] }, skill_params: { type: object } }, required: [skill_name, skill_params] } }模型输出“调用skill_nameweb_searchskill_params{...}”之后主循环直接丢给executor执行再把SkillResult塞回对话上下文。这种做法的最大好处是当技能数量增加模型的function列表不会膨胀因为永远只有一个call_skill函数真正的选择发生在参数里。模型的任务从“从50个工具里挑一个”变成“从文本描述的技能列表里挑一个”准确率高很多。不过这种方案也有代价描述字段必须精心编写。我在skill.yaml里加过一个字段叫_agent_usage_hint里面写了一段供模型阅读的“技能选择指南”例如“当用户想要了解今天天气时优先使用weather_lookup当用户提到‘最近’‘最新’时优先使用web_search”。这段hint不会注入给模型但会自动拼接到call_skill函数的description里。实测下来这个字段能明显降低模型误选技能的概率。3.3 技能间调用与数据流设计高级一点的场景技能间会互相调用。比如“写一篇今日科技新闻简报”内部需要先调web_search拿新闻再调summarize做摘要最后调note_writer保存。技能间调用有两种方案。方案一技能A内部直接import技能B的run函数然后调用它。这种做法耦合最强但违反了自包含原则skill B的参数校验、错误处理都被跳过。方案二在SkillContext里注入executor的引用技能A内部通过executor.execute调用技能B。这样技能B的完整生命周期依然受管控。方案二是我推荐的但要注意循环依赖。比如A调用BB又依赖A就会死锁。我在registry的_validate_dependencies里用DFS检测了环如果发现循环依赖就直接报错。技能间传递的数据要符合B的input_schema所以A在调用B之前需要把数据重新组装成B需要的样子不能直接把A内部的数据结构丢过去。还有一个数据流细节技能内部产生的中间数据默认不回传主对话。比如web_search拿到的原始HTML模要转成模型理解的结构化摘要。我在SkillResult里设计了一个visible_data字段只有标注了对用户可见的数据才会回传其余都留在本地日志里。这样做能大幅减少token浪费。4. 常见问题与排查技巧实录4.1 模型总是选错技能怎么调优这是技能化改造后最常遇到的问题。我接手过一个项目技能库里有“获取订单状态”和“获取物流信息”两个技能模型经常把查物流的请求打到订单状态上。排查后发现原因很简单两个技能的description太像了。我的调优技巧有三个按优先级排列在description里写清楚“不适用”的场景。比如订单状态里加一句“本技能只返回订单审核、支付、完成状态不返回快递流转信息”。负向约束有时候比正向描述更有效。增加“技能选择指南”字段用if-then句式指导模型。例如“如果用户询问‘快递到哪了’‘发货了吗’使用物流查询技能不要使用订单状态技能”。如果还是选错考虑把两个技能合并成一个用参数区分。毕竟模型面对选择时选项越少越稳。4.2 技能执行超时与重试策略技能里最常见的问题就是外部请求挂起。我的默认策略是给每个技能设置超时默认10秒可在上下文配置里覆盖。实现时不是依赖Python函数内部自己超时而是在executor层用asyncio.wait_for包住run调用。try: result await asyncio.wait_for( skill_def.run_func(self.context, **validated), timeoutself.timeout ) except asyncio.TimeoutError: return SkillResult(successFalse, error技能执行超时, retryableTrue)重试策略不是无脑重试。我规定只有retryable错误的技能才重试且最多重试2次两次间隔用指数退避1秒、2秒。对于幂等性差的技能比如“创建订单”重试会重复下单这类技能要把retryable标记为False。怎么标记我在skill.yaml里增加了retry_policy字段配置allowed和max_retries代码里读取这个配置而不是硬编码。4.3 技能热更新与版本冲突开发过程中免不了频繁改技能代码。最开始我的做法是每次改完重启整个服务后来技能多了重启一次要好几十秒实在受不了。于是做了热更新通过文件监听发现某个技能目录的skill.yaml或run.py变更后自动重新import并替换注册表里的条目。热更新有一个天坑Python的importlib缓存。直接重新import_run.py拿到的还是旧模块必须先用sys.modules.pop把旧模块踢掉。正确姿势module_name fskills.{skill_folder.name}.run if module_name in sys.modules: del sys.modules[module_name] module importlib.import_module(module_name)另外热更新期间正在执行的旧技能进程可能还没跑完我是用引用计数解决的在executor执行前给技能定义加一把读锁热更新时持有写锁等正在跑的技能执行完再替换。如果你的项目并发不高可以简单点每次热更新前sleep两秒图个省事。版本冲突也踩过一次。同事给“摘要”技能升了级但另一个技能里显式依赖了旧版摘要的行为结果输出格式对不上。后来我在skill.yaml里加了api_version字段并在调用依赖方时检查版本兼容范围不兼容就直接抛异常宁可报错也不能静默输出错误结果。4.4 可观测性日志与排错实战技能化之后一个请求可能会跨多个技能排错难度上升。我的做法是围绕trace_id做全链路日志。每一个SkillResult生成时都带trace_id主代理收到一个请求就生成一个根trace_id然后所有技能调用都带上这个trace_id。具体落地上我在SkillContext里维护一个trace_id技能内部所有日志都通过context.logger输出这个logger在初始化时绑定了trace_id和技能名。于是日志长这样2025-04-05 10:22:31 INFO [tracea92fd] [skillweb_search] 查询: agent skills 2025-04-05 10:22:31 INFO [tracea92fd] [skillweb_search] 发起搜索请求 2025-04-05 10:22:32 INFO [tracea92fd] [skillsummarize] 开始摘要排查问题时我习惯用一条命令查全链路日志grep tracea92fd agent.log除了日志我还会为每个技能记录指标调用次数、平均耗时、成功率、错误类型分布。这些数据对判断哪些技能需要优化非常关键。我见过一个“生成图表”技能平均耗时8秒但其他技能都在1秒以内一查是它内部调用了一个慢速模型及时替换掉了。4.5 上下文管理防止技能输出污染主对话最后一个高频问题技能执行过程中产生的大量中间输出会被模型当成上下文读进去导致后续对话质量下降。比如一个技能内部做了三次搜索返回了三十条结果主模型全看一遍既费token又分心。我的解决方案是分两级上下文。一级是“可见摘要”技能向主对话回传的数据必须经过精简通常是结果列表的前N项。另一级是“完整上下文”存到本地或向量库里模型需要细节时再通过检索技能获取。这个设计让我在保持主对话轻量的同时又能随时深挖细节。实操中我在executor里对SkillResult.data做一次大小检查超过约1500字符就强制走摘要压缩。这个阈值可以配置但1500是我试过多个项目后的甜点值——模型不会遗漏关键信息token也省很多。5. 一些加分设计与扩展方向5.1 技能的市场化能力包与分享机制当技能库积累到一定数量你会发现“技能”本身可以成为一种可分发的资产。我后来在项目里加入了打包功能一个技能目录能被打成zip包内含skill.yaml、run.py、prompt.md、requirements.txt、README.md。别人拿到这个包解压到skills目录注册器自动识别技能就能跑了。为了确保可移植性我规定了技能代码里不能出现绝对路径所有外部资源都必须放在技能目录内部或者通过外部服务获取。这一条避免了很多“在我机器上能跑”的问题。给他人分享技能时我喜欢附上一张“技能卡”用表格写明输入输出、依赖项、示例调用接收方一眼就能判断是否适合自己。5.2 技能的快速评测体系技能开发和普通功能开发最大的不同是技能的结果很难用布尔值衡量对错。我建了一个评测集针对每个技能准备十来条典型输入每条标注了“预期行为描述”。每次改动技能后跑一遍评测集人工核对输出是否合理。这个操作很土但确实有效。更高级一点的可以用LLM当裁判。把技能输出给一个评测模型让它按照既定标准打分。比如摘要技能评测模型根据“是否正确压缩核心内容”“是否包含原文不存在的观点”等维度打分。自动化评测跑起来后技能库的迭代速度才能提上去否则每次改需求都要手动回归迟早累垮。5.3 从技能库到智能体编排平台技能库一旦稳定自然会长出编排需求多个技能如何串成一条工作流什么时候可以并行执行多个技能这些需求已经超出技能本身属于智能体编排平台的范畴。我目前正在做的扩展是让agent能生成执行计划计划由技能节点组成支持并行和串行依赖。executor升级成plan_executor先解析计划再调度技能。技能之间通过一个简单的数据总线交换信息每个技能声明自己“产出”和“消费”的数据类型总线负责路由。这条路走通之后“agent-skills”就不再只是技能管理器而是一个轻量级的智能体运行时。回过头来看最初那个“让技能可复用”的小目标最终撬动了整个Agent工程的架构演进。这也是我觉得这个方向值得持续投入的原因——低成本、强复用、可积累正是工程化最需要的东西。