资讯动态

Agent技能框架agent-skills:设计、核心机制与实战解析

发布时间:2026/9/16 8:20:11 来源:尧图企业网站定制
做AI Agent方向的开发已经有段时间我越来越确信一件事模型本身的推理能力再强如果没有一套设计得当的技能体系让它去调用真实的工具和数据Agent 就只是一个精致的聊天框。这也是我启动 agent-skills 这个项目的直接原因。它本质上是一套面向 LLM Agent 的轻量技能框架核心解决三件事怎么把业务能力抽象成模型能看懂、能调用的技能怎么让这些技能可以复用、热插拔以及怎么让 Agent 在复杂的多步任务里稳定地编排技能而不是中途跑飞。这篇文章我会把 agent-skills 的整体设计、核心机制、关键代码和踩坑记录都摊开来讲。不管你是正在摸索 Agent 工程化的开发者还是已经在做企业内部 AI 工具链、希望给团队沉淀一套可复用能力层的人这篇内容应该都能给你一些比官方文档更实在的参考。我会尽量把每个决策背后的为什么也说清楚而不是只丢一堆代码给你。1. 项目定位agent-skills 到底在做一件什么事1.1 从能聊天到能干活Agent 缺的是一层技能抽象先聊一个很常见的问题。很多人第一次做 Agent 应用时习惯直接把所有工具函数塞给模型Prompt 里写你有这些工具可用需要时调用。Demo 阶段没问题但一旦工具数量过了 10 个、20 个模型就开始乱了要么选错工具要么参数填得驴唇不对马嘴要么干脆自己脑补一个函数名出来。问题出在哪出在你只给了模型一堆零散的函数却没有给它一个结构化的、自描述的技能层。函数签名是给编译器看的不是给 LLM 看的。模型需要的是这个技能是干什么的、什么时候该用、参数应该怎么填、可能的边界条件是什么。agent-skills 的全部工作就是把这一层东西补上。简单说这个项目定义了一个技能Skill的标准结构提供注册、发现、调度、执行、回滚的完整生命周期管理。业务团队只需要按照约定写技能Agent 就能在运行时动态感知并调用这些技能。它不是一个重框架不绑定特定的大模型厂商也不强制你用某种 Agent 编排器它只做技能管理这一层最核心的脏活累活。我在设计时有一个明确的原则技能必须是描述性的、自包含的。描述性意味着每个技能都带有一套完整的元信息模型光靠读描述就知道怎么用自包含意味着技能不能依赖外部的隐式状态所有需要的信息要么通过参数传入要么技能自己负责获取。1.2 这个项目聚焦并想解决的四个核心问题具体拆解下来agent-skills 的定位很聚焦主要就是为了解决四类问题这也是我这段时间做企业级 Agent 项目时被反复折磨的痛点第一是技能的复用问题。同一个发邮件能力A 项目里写了一套B 项目里又写了一套参数风格还不一样最后模型在两边表现完全不一致。agent-skills 希望把技能做成标准件像乐高积木一样即插即用一份定义到处跑。第二是模型与工具之间的翻译问题。原始 API 的参数往往是工程师风格的to、cc、bcc、htmlBody模型未必理解htmlBody和textBody在什么场景下应该用哪个。技能层要做的是把这些东西转换成模型容易理解、不容易想歪的描述和约束。第三是技能的编排问题。单技能简单多技能协作难。Agent 经常需要先查库存再算价格最后生成订单这种多步操作每一步该调哪个技能、参数怎么上下游传递如果没有一套编排机制最后代码会变成一团乱麻。我在 agent-skills 里组合了一套轻量链式调度的实现后面会详细说。第四是技能的可观测性。生产环境里 Agent 调用了什么技能、每个技能花了多长时间、参数和返回是否符合预期这些必须有完整的轨迹记录。不然出了问题都没法排查——模型说它调了但你不知道它具体传了什么参数进去。这个项目适合谁如果你正在自己搭 Agent 架构不想被某个封闭的厂商框架绑死或者你需要为企业内部沉淀一套统一的AI 可调用能力清单那么 agent-skills 的思路和代码都会对你有用。2. 技能体系的整体设计与架构思路2.1 技能的三种粒度原子技能、组合技能、工作流我最早设计技能结构时只分了两类简单技能和复杂技能。后来在真实场景里跑了一段时间发现这种二分法太粗糙无法描述真实的业务复杂度。最后收敛成三种粒度原子技能、组合技能和工作流技能。原子技能是最小粒度的能力单元通常直接封装单个 API 或函数调用。比如根据订单号查询订单状态计算两个日期之间的工作日天数生成 UUID。原子技能要求做到职责单一、无副作用或副作用可控。它们是整个技能体系的基石。组合技能是在原子技能之上的封装内部会调用多个原子技能但对调用方暴露的仍然是一个简单接口。典型的例子是生成周报这个技能内部可能依次调用了获取本周待办聚合工时数据调用模板渲染三个原子技能。组合技能的关键在于内部编排逻辑要稳定不能让模型干预太多中间步骤否则输出随机性太大。工作流技能是更复杂的场景化能力强调状态流转和分支判断。比如处理退款申请就是一个工作流技能里面要根据订单状态、支付渠道、金额大小走不同分支有些分支需要人工审批。这种技能我一般建议尽可能把它内部的判断逻辑收敛到代码里只给模型暴露明确的决策点而不是让它自由发挥。这三层结构带来的好处是不同的技能有不同的测试策略和稳定性要求。原子技能要 100% 稳定组合技能要 95% 以上稳定工作流技能允许人工介入兜底。这种分级思路也直接影响了后面调度器的设计——等会儿会讲到。2.2 为什么用 JSON Schema 描述技能而不是直接写死函数这是一个我在社区里被问过很多次的问题。你要是直接用 Python 或 TypeScript 写一个send_email(to, subject, body)函数给 Agent代码当然是类型安全的但模型看到的是什么它看到的是函数名、参数名以及你在 Prompt 里可能补的一句概述。这信息量太少了。agent-skills 里每个技能都附带一个完整的 JSON Schema用来描述参数结构、类型、必填项、枚举值、依赖关系等。模型尤其是支持 function calling / tool use 接口的模型会在运行时读取这份 Schema 来决定是否调用以及如何填参数。JSON Schema 是模型和工具之间的通用语言它不是给人看的那种接口文档而是给 LLM 做结构化决策用的。举一个实际例子。有一个技能是查询销售数据参数里有个granularity字段如果只在类型里写string模型很可能填成 month 或者 monthly 或者 月每次都不一样。但在 JSON Schema 里你可以明确定义enum: [day, week, month]并加一段描述说明什么场景该用哪个值。这个约束效果立竿见影参数错误率会明显下降。除了字段类型和枚举JSON Schema 还支持oneOf、anyOf、嵌套对象和数组结构。这意味着技能可以描述非常复杂的业务对象。比如创建订单这个技能它的参数可能涉及customer、items、shipping_address、payment等多个嵌套结构每个结构内部还有各自的校验规则。这些信息全部通过 Schema 传给模型模型就能准确地把自然语言指令映射成结构化参数。我还在 Schema 里加了一个自定义字段x-experience专门用来写这个参数在实际使用中的常见坑。比如某个时间参数描述里会写注意此处需要 UTC 时间不要传本地时间如果调用方在 UTC8 时区请先做转换。对模型来说这种自然语言的提醒往往比干巴巴的类型定义更有效。2.3 技能描述的艺术模型能不能选对一半看命一半看描述有句话说得很对给模型写的技能描述本质上是在给一个聪明但没有常识的实习生写工作手册。你必须假设它对你们公司的业务术语一无所知同时又假设它能力很强只要描述到位就能正确执行。技能描述我总结了一套固定模板每条描述都包含四部分执行条件什么时候该用这个技能、执行后果调用后会发生什么有没有副作用、参数语义每个关键参数怎么填有什么坑、边界情况什么条件下不要用这个技能。举个例子我之前设计过一个发送营销短信的技能。第一版描述只写了给指定用户发送营销短信参数包括手机号和文案结果模型在用户查询给我发个验证码的时候也调用了这个技能。这显然是灾难级的误用。后来我把描述改成本技能用于发送批量营销短信仅适用于用户已明确授权接收推广信息且当前会话语境为营销推广的场景。不要在身份验证、安全提醒、事务通知场景下使用本技能这些场景应调用发送验证码或发送通知类的技能。改完之后误用率几乎降到零。这里有一个容易被忽视的设计原则技能之间要有清晰的边界描述而不仅仅是各自独立的正向描述。你不仅要告诉模型这个技能是干什么的还要告诉它这个技能不是什么、和哪些技能容易混淆、什么情况下不要选它。模型在多个候选技能中做选择时这种负向描述的作用往往比正向描述更大。另一个经验是技能描述要学会用业务场景的语言而不是技术语言。比如检查 API 配额这种描述模型能理解但在真实业务里用户会说为什么今天不能发请求了——这时候模型需要联想到检查 API 配额这个技能。所以描述里可以加一句当用户反馈发不出消息、接口报错或被限流时可使用本技能查看当前项目的 API 调用余量。3. 核心机制实现注册、调度与执行3.1 技能注册中心让模型动态感知可用的能力集agent-skills 里有一个全局的技能注册中心SkillRegistry所有技能在应用启动时或者运行中动态注册进来。注册中心维护了一份完整的技能清单并且会实时生成一份压缩后的技能目录注入到每次模型调用的上下文里。为什么要做动态注册而不是写死配置因为在实际项目中技能列表往往是不断变化的。新业务上线要加技能旧接口下线要摘除技能不同客户有不同权限需要看到不同技能集。如果每次变更都要改代码发版那开发效率就太低了。动态注册配合权限过滤能实现同一个 Agent 实例对不同用户暴露不同的技能集合。看一下注册的关键代码实现TypeScript 版本type SkillHandler (params: Recordstring, any, context: ExecContext) PromiseSkillOutput; interface SkillDefinition { name: string; version: string; description: string; tags: string[]; parameters: JSONSchema; handler: SkillHandler; timeout?: number; requiredPermissions?: string[]; } class SkillRegistry { private skills new Mapstring, SkillDefinition(); register(def: SkillDefinition) { if (this.skills.has(def.name)) { throw new Error(Skill already registered: ${def.name}); } validateJsonSchema(def.parameters); this.skills.set(def.name, def); } unregister(name: string) { this.skills.delete(name); } listForUser(user: User, permissionService: PermissionService): SkillSummary[] { return [...this.skills.values()] .filter(s permissionService.canUse(user, s.requiredPermissions ?? [])) .map(s ({ name: s.name, description: s.description, parameters: s.parameters })); } get(name: string): SkillDefinition | undefined { return this.skills.get(name); } }这里有个细节值得说明注册时我会做一次 JSON Schema 的预校验如果 Schema 本身格式不合法当场抛错而不是等到运行时报。这个预校验成本很低但能把大量低级错误拦截在开发期。比如某个参数类型写错了、某个枚举值不是数组这些错误一上线就会导致模型选技能时解析失败甚至崩溃。权限过滤这一层也很关键。企业内部很多技能涉及敏感操作比如发送对外邮件删除生产环境数据获取客户隐私信息。这些技能不应该对所有用户开放。权限最小的实现是在注册中心查询时直接过滤配置管理都交给权限服务。这一层做干净了后续做多租户、审计、合规都会省很多事。3.2 调度器选型什么时机让模型自己选什么时候走规则路由技能调度是整个系统里最容易翻车的环节。我最早天真地把所有技能都开放给模型自由选择结果遇到一个问题当技能池超过 20 个时模型的选择准确率会肉眼可见地下降经常把查用户信息和查用户的订单搞混或者把一个专业性很强的技能描述理解偏了。后来我把调度策略改成了分层路由。具体来说是这样的系统先过一个轻量级的意图分类器把用户意图大致分到几个域比如数据查询交易操作内容生成系统管理然后每个域再向模型开放对应的技能子集。分类器本身可以用小模型也可以直接用规则匹配成本很低但能把技能选择范围一下子缩小到原来的三分之一以下准确率提升非常明显。在实现上agent-skills 的调度器支持三种模式第一种是模型自主选择模式适用于开放域问答和通用助手场景。系统把完整技能目录塞给模型由模型决定调用哪些技能以及调用的顺序。这种模式最灵活但需要技能描述写得非常清楚且技能数量不能太多。第二种是规则路由模式适用于流程稳定的业务场景。比如客服系统里用户说我要退款规则路由直接把请求转到退款处理工作流技能不经过模型决策。这种模式牺牲了灵活性但换来了稳定性和可预测性而且能显著降低响应延迟。第三种是混合模式也是我在大部分生产项目里实际采用的方案。先用规则匹配一次命中就走固定流程没命中再走模型自主选择。如果模型选择的结果置信度低于阈值通常在首次选择时会返回一个confidence字段不过很多模型不提供这个字段我会要求模型先输出一个plan再执行就退回人工兜底或者让模型再次确认。这套调度策略的代码核心其实很短但要把整个决策过程都记录下来非常关键。我实现了完整的 trace 日志每次调度的输入、候选技能列表、路由结果、最终技能选择都会落盘。生产环境排查问题的效率完全靠这些日志撑着。3.3 执行器设计上下文管理、超时控制与幂等回滚执行器是真正干活的地方也是踩坑最多的地方。第一个坑是上下文传递。技能和技能之间经常需要共享数据比如查询订单技能返回了订单号生成发票技能才能用。如果用全局变量传并发一高就串数据了每次全量传上下文又会导致 token 消耗爆炸。agent-skills 的做法是维护一个SkillContext对象它会记录当前执行链路的关键中间结果并且支持两种读取方式精确 KEY 读取和语义化搜索读取。精确 KEY 读取适合上游技能和下游技能之间有明确的数据契约的场景语义化搜索则是在没有明确契约时让调度器从上下文中检索相关信息再决定如何传给下一个技能。这两种读取方式可以组合使用代码里大概是这样的class ExecContext { private state new Mapstring, unknown(); private eventLog: TraceEvent[] []; private maxContextLength 4096; set(key: string, value: unknown) { this.state.set(key, value); } getT(key: string): T | undefined { return this.state.get(key) as T | undefined; } // 语义化搜索适合不确定 key 是否存在时的兜底 semanticGet(prompt: string, maxResults 3): Array{ key: string; value: unknown } { // 实际实现中会调用 embedding 模型进行相似度检索 return this.searchInState(this.state, prompt, maxResults); } trace(event: string, data?: unknown) { this.eventLog.push({ event, data, ts: Date.now() }); } }这个上下文对象里我会定期清理不用的数据避免无限膨胀。通常每一轮技能调用结束后会保留当前链路中最新的 N 个关键变量超过maxContextLength的部分按照 LRU 策略淘汰。这里要小心的是不能把模型后续决策需要的敏感数据淘汰掉所以我会给部分变量加一个persist标记这样的变量在整条链路结束前都不会被自动清理。第二个坑是超时控制。LLM 推理本身就慢一个技能如果还要调外部 API、查数据库很可能整体超过 10 秒。不同技能的耗时预期差异极大有的技能 200ms 就应该返回有的技能要跑好几个小时。agent-skills 允许在技能定义里单独的timeout配置。超时后执行器会取消当前技能的 Promise并且立即触发一个错误事件通知调度器进行降级处理。第三个坑是幂等回滚。凡是涉及写操作的技能都需要考虑如果这个技能被调用了两次会发生什么。比如给用户账户增加 100 积分这个操作如果模型因为网络重试或者推理重复执行了两次用户的积分就被加了两次这是不能接受的。我在技能定义里增加了一个idempotencyKey的可选参数建议所有写操作技能都要校验幂等键并在执行器层面做了去重同样的幂等键在同一个会话里只允许执行一次重复请求直接返回第一次的执行结果。4. 实操演示从零到一写一个可用的业务技能4.1 定义一个销售数据报表生成技能前面讲了不少抽象设计可能有点干这一节我用一个完整的业务技能来串一遍。假设我们接到一个需求要让 Agent 能够根据自然语言问题自动从销售数据库里查询数据并生成一张 Markdown 格式的报表。这个技能我们在 agent-skills 框架里可以这么定义。首先分析这个技能的组合属性它其实是组合技能内部涉及三个子任务——解析查询条件、执行 SQL、渲染报表。前两个子任务如果让模型一步到位去写 SQL查复杂业务库时错误率会很高所以我会把查询条件规范化这一步拆出来用规则来做而不是靠模型自由发挥。先定义参数 Schemaconst generateSalesReport { name: generate_sales_report, version: 1.2.0, description: 根据用户的需求生成销售数据报表支持按时间范围、区域、产品线、渠道四个维度过滤。 当用户问最近一个月的销售额是多少华东区 Q3 卖了多少台或要求生成一份本周销售周报时使用本技能。 注意本技能只读数据不执行任何写入操作。如果用户要求修改/删除销售数据请改用 modify_sales_data 技能。, parameters: { type: object, properties: { date_range: { type: object, properties: { start: { type: string, description: 开始日期格式 YYYY-MM-DDUTC 时间 }, end: { type: string, description: 结束日期格式 YYYY-MM-DDUTC 时间 } }, required: [start, end], description: 查询时间范围必填。如果用户只给出相对时间请根据当前日期推算。 }, region: { type: array, items: { type: string, enum: [华北, 华东, 华南, 西部] }, description: 区域过滤条件可选。不传则查全部区域。 }, product_line: { type: array, items: { type: string, enum: [手机, 电脑, 配件] }, description: 产品线过滤条件可选。不传则查全部产品线。 }, group_by: { type: string, enum: [day, week, month, region, product_line], description: 汇总粒度可选。决定报表的分组维度。如果用户要求按月份看趋势填 month。 } }, required: [date_range, group_by] }, handler: async (params, ctx) { // 实施步骤 // 1. 解析并校验参数 // 2. 拼接 SQL这一步用固定的模板不允许模型直接传 SQL // 3. 执行查询 // 4. 将结果渲染为 Markdown 表格 } };这个定义里有几个细节是经过很多次调试才定下来的。第一我在描述里明确了只读属性和混淆技能提示这就避免模型把生成报表和修改销售数据搞混。第二date_range我在描述里强调了UTC 时间因为实际项目里这个问题至少出过三次事故模型总是喜欢把本地时间直接传进来。第三group_by字段是必填的但模型经常不知道怎么填所以我在描述里给了很具体的示例如果用户要求按月份看趋势填 month实践证明这样的提示效果比干巴巴的枚举值列表好很多。4.2 技能内部实现与模型意图解析的配合handler内部的实现最容易被低估的部分是把自然语言查询转化为 SQL 的中间层。最初我尝试让模型直接生成 SQL然后丢到数据库执行结果在生产环境出了大事模型生成的 SQL 存在语法错误、把表名写错、甚至有一次生成了DELETE FROM语句差点把数据清掉。虽然框架层可以加只读约束但更稳妥的做法是从源头上就不让模型直接操作 SQL。我在 handler 里做的事情是把模型需要决策的空间缩小到选参数这个层面而不是生成代码这个层面。最终执行的 SQL 是从一套预先写好的模板里拼出来的。比如按日期范围过滤订单明细模板长这样SELECT trade_date, SUM(amount) as total_amount FROM sales_orders WHERE trade_date {start} AND trade_date {end} AND region IN ({region_list}) AND product_line IN ({product_line_list}) GROUP BY trade_date ORDER BY trade_date;{region_list}和{product_line_list}是从参数里转义后拼进去的所有值都必须经过白名单校验凡是不在枚举值列表里的输入直接丢弃。这保证了即使模型填了奇怪的东西最终执行的 SQL 也是安全的。这一步完成后把 SQL 执行结果传给渲染模块。渲染模块我直接用了一个模板函数把数据转成 Markdown 表格再追加一行总结总销售额、环比变化率、同比变化率。这部分逻辑用一行额外的模型调用会带来不确定性所以我把它做成纯代码计算。数据报表这个场景计算结果必须 100% 精确不适合让模型来做算术。4.3 技能调试离线测试、Mock 外部依赖与 echo 模式技能写完之后最痛苦的部分来了调试。一个大模型应用里技能本身有 Bug 和模型调用方式有问题经常混在一起难以区分。为了把这两类问题剥离开agent-skills 里我实现了三种调试模式。第一种是纯离线模式。跳过真实的 LLM 推理直接用预先录好的用户请求做输入然后把 handler 跑一遍只检查技能自身的逻辑是否正确。这等价于传统软件开发里的单元测试。很多技能逻辑问题——比如 SQL 拼接错误、空值处理不当、分页参数错误都能在这一层发现。我在项目里会为每个技能强制要求至少一个离线测试用例用例覆盖正常路径、边界参数、异常参数三类场景。第二种是Mock 外部依赖模式。技能大多要调外部 API 或数据库调试时不可能每次都连真的环境。agent-skills 支持在技能定义里声明它依赖的外部服务并通过依赖注入的方式在测试环境替换成 mock 实现。比如上面的报表技能测试环境里我会把数据库客户端 mock 成固定返回三行数据这样跑一遍就能确认渲染逻辑没问题而不用真的连库。第三种是echo 模式也是我最常用来排查模型到底干了什么的模式。在这个模式下所有技能的 handler 不会真实执行而是把收到的参数原样返回同时记录一份完整的请求日志。这样我可以快速验证模型有没有在正确的时候调用正确的技能参数填得对不对是不是反复调用同一个技能这类问题靠观察日志就能定位不需要真的去查数据库或者发邮件排查效率非常高。5. 常见问题复盘5.1 模型总是选错技能怎么排查这是被问得最多的问题没有之一。遇到模型选错技能我一般不会急着去调模型参数而是先看 trace 日志把模型当时的输入上下文完整拉出来回答三个问题模型在这个时刻看到了哪些技能描述用户完整的对话历史是什么模型实际选了哪个技能、为什么它会觉得这个技能合理排查的时候有个非常容易被忽略的点技能描述的先后顺序会影响模型的选择。有些模型对排在前面的技能描述有偏好如果两个技能描述语义比较接近排在后面的往往被忽略。我做过一个实验把生成退款单和生成发票两个技能调换顺序模型的选择结果也跟着调换而且模型自己完全感知不到这个问题。这个问题的解法是在技能描述里故意增加互相区分的负向描述而不是指望调整顺序。如果确认描述没问题、顺序也调整过模型还是选错那就要检查是不是技能的参数 Schema 存在歧义。比如日期参数在 A 技能里是字符串格式在 B 技能里是时间戳格式模型的输入又都是自然语言它可能就猜不准哪里该填什么。这种时候我会把相关的描述统一改写全部规范成同一种格式并在描述里写清楚。还有一种场景是模型知道自己该调用某个技能但不知道技能的准确名字。有些模型的 function calling 能力比较弱名字稍微长一点就截断了或者干脆自己拼一个相似的名字出来。这种情况下不要怪模型而是在技能名设计上做文章。我给技能命名有一个约定动词开头 下划线 业务对象比如query_user_profile、create_refund_order全小写不加多余的前缀后缀。名字保持在 28 个字符以内实测这种命名方式在主流模型上的识别成功率最高。5.2 参数校验和类型对齐为什么模型填的参数总是带着多余空格模型填参数的时候经常出现一些人类不会犯的低级错误比如在字符串首尾加上多余空格、把日期格式从2024-05-01写成2024年5月1日、数字填成字符串。这些问题很小但积累起来会触发很多难排查的隐性 Bug。我的做法是做一个参数标准化管道在技能 handler 被真正调用之前先把模型原始的 JSON 参数过一遍清理和归一化function sanitizeParams(raw: Recordstring, unknown, schema: JSONSchema): Recordstring, unknown { const result: Recordstring, unknown {}; for (const [key, propSchema] of Object.entries(schema.properties ?? {})) { let val raw[key]; if (val undefined) continue; if (typeof val string) { val val.trim(); if (propSchema.format date /^\d{4}年\d{1,2}月\d{1,2}日$/.test(val)) { // 手动转为 YYYY-MM-DD val val.replace(/(\d{4})年(\d{1,2})月(\d{1,2})日/, $1-$2-$3); } } if (propSchema.type number typeof val string !isNaN(Number(val))) { val Number(val); } result[key] val; } return result; }这段代码看起来简单但实际解决问题的数量远超预期。以前模型传日期经常是中文格式SQL 直接拼接进去就会出语法错现在先归一化成标准 ISO 格式后面所有环节都省心。值得一提的是参数清理绝不能做过度转化如果一个参数字符串是用户提供的原文比如搜索框里输入的内容就不要动它不然会改变语义。所以我在 Schema 里加了一个x-raw自定义标记标了x-raw: true的字段不做 trim 以外的任何处理。5.3 并发安全和技能间的资源竞争最后聊一个比较进阶的问题当 Agent 多个实例同时跑技能内部如果有共享资源比如数据库连接池、Redis 连接、内存缓存很容易出现资源竞争。我印象最深的一次事故是一个查报告的技能内部用了模块级的全局缓存对象上线后发现偶尔会出现 A 用户看到 B 用户的数据。排查了半天才发现是缓存对象被多个实例共享没有做会话隔离。agent-skills 里对这个问题做了几个约束。第一技能 handler 不允许使用全局可变状态所有状态必须放在ExecContext里这样天然做到了按会话隔离。第二技能与外部系统的连接数据库连接、HTTP 客户端统一通过依赖注入传入每个会话可以拿到自己独立的连接实例。第三对于需要共享的资源比如限流器、计数器提供了原子操作的原语封装避免多个并发调用同时读写导致数据不一致。另外一个常被忽略的问题是技能重入。工作流技能在编排过程中可能会调用自身比如递归处理嵌套结构如果不做重入保护就会无限循环。我在执行器里给每个技能加了一个最大调深限制默认是 10 层超过这个深度强制抛错。这个限制平时用不到但一旦模型设计了一条错误的循环链路它能帮你保住服务器的 CPU。6. 实战经验分享这个项目最让我意外的几个结论做 agent-skills 这几个月有几个结论是完全超出我最初预期的这里分享给正在做同类项目的朋友。第一技能描述的投入产出比极高。我粗略统计过花在优化技能描述上的时间和最终模型调用准确率之间的关系接近线性。写技能时多花二十分钟把描述精修一遍能把生产环境的错误率降低一半以上这远比调 prompt 模板或者换模型版本来得有效。描述不是一次写对的是要在 trace 数据的反馈里持续迭代的我建议把它当做一个持续优化的过程而不是一次性交付物。第二技能不是越多越好。技能池越庞大模型的选择压力越大准确率下降得就越快。我现在遵循一个原则优先组合而不是新增。如果一个操作可以由两个已有技能组合完成就不新建第三个技能如果两个技能描述语义太接近、经常被混淆我会考虑合并成一个技能用一个参数来区分具体操作。保持技能数量在 15 到 20 个以内模型的表现通常最稳定。第三给模型做决策的辅助笔记非常重要。agent-skills 的x-experience字段一开始只是随手加的后来发现这是全项目里对准确率提升最大的功能点。模型本身缺少很多业务领域的隐性常识你在参数描述里把这些常识写清楚比如下单金额超过 5000 需要走审批流程模型就能在配置参数时自动调整结构这种效果是纯靠优化主 prompt 很难达到的。最后说一句Agent 项目的复杂度很大程度上是一种隐形复杂度。一个单独的技能看着简单但技能之间的边界划分、上下文传递、错误恢复、权限控制全都要在设计层面提前想好。这个项目不会让你的 Agent 直接变聪明但能让你在 Agent 变复杂时不至于崩溃。如果你正在做类似的尝试欢迎到 GitHub 搜 agent-skills 一起交流也建议直接从你自己的一个真实业务技能开始把它落到框架里跑通了再慢慢扩展。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价