最近一段时间我一直在做一件事:把公司里一堆散落的自动化脚本,重构成一个真正意义上能“自己干活”的全能 Agent。初步跑通之后我总结出一个规律——凡是能用好的 Agent,背后一定有一套设计良好的 AI Skills;凡是动不动就翻车的 Agent,问题九成出在技能边界没划清楚。这篇文章不聊概念,只聊我实际在腾讯云上把一个 Agent 从“会聊天”变成“能干活”的完整过程,包括 Skills 怎么设计、怎么部署、怎么排障,全部是可落地的最佳实践。
如果你已经知道 Agent 是什么,但卡在“框架搭好了却不知道该让它干什么”的阶段,或者你有一些 Python 脚本想封装成 Agent 能调用的能力,又或者你正在纠结 Skill 和 Agent 到底怎么分工——那么这篇文章就是给你写的。
1. 先想清楚:Agent 和 Skill 到底是什么关系
1.1 我踩过的一个坑
最早我做 Agent 的时候,犯过一个特别典型的错误:把需求一股脑全塞进 System Prompt 里。当时要做一个运维巡检助手,我在提示词里洋洋洒洒写了两千多字,告诉模型看到 CPU 高了要怎么办、看到磁盘满了要怎么办、看到日志报错又要怎么查。结果上线第一天就翻车了——模型确实“知道”所有规则,但它不知道如何去调用真实的服务器 API,也不知道该在什么时机触发检查,最后只是凭空生成了一堆看似合理但完全没法执行的建议。
后来我才明白问题出在哪:Prompt 只能让模型“知道”,无法让模型“做到”。Agent 的本质是一个决策和调度系统,它需要外部能力来真正完成动作。这些外部能力,就是 AI Skills。
1.2 Skill 是一张“技能卡”,Agent 是持卡人
我现在的理解方式比较接地气:把 Agent 想象成一位项目经理,把 Skill 想象成一张张技能卡。项目经理自己不亲自写代码、不发请求,但他看到任务之后,会从手里的技能卡里挑出最合适的一张,照着卡上的说明去执行。
Skill 通常包含三样东西:
- 一个可以被程序调用的函数或服务(真正干活的代码)
- 一份参数 Schema(告诉 Agent 这个技能需要什么输入,比如 IP 地址、时间范围、阈值)
- 一段自然语言描述(告诉 Agent 这个技能是干嘛的、什么时候用它、什么时候别用它)
这三者缺一不可。没有描述,模型不知道该选它;没有 Schema,模型传参会靠猜;没有服务本体,整个 Skill 就是个空壳。
1.3 为什么要拆 Skill,而不是全塞进 Prompt
这个问题我反复被问到过。很多人觉得,既然大模型能力这么强,我写清楚它自己去执行不就行了?答案是不行,原因有三点:
第一,可测试性。Prompt 是黑盒,你没法单独验证某一段逻辑是否正确。但 Skill 是一个独立函数,你可以像测普通接口一样给它喂测试数据,断言返回结果。哪个 Skill 挂了,单独修哪个,不需要把整段 Prompt 重新调一遍。
第二,可复用性。一个“日志查询”Skill,既可以让运维 Agent 用,也可以让数据分析 Agent 用,甚至可以让客服 Agent 用它来查订单日志。如果你把逻辑写死在 Prompt 里,换个场景就得重新写一段。
第三,可灰度性。Skill 可以单独升级、回滚、限流。今天把一个 Skill 从 v1 升到 v2,完全不影响 Agent 的其他部分。这在生产环境里太重要了——我经历过一次 Prompt 改了一个词,结果整个 Agent 回复风格全变的诡异事故,但换成 Skill 之后就再没出过这种问题。
2. 全能 Agent 的 Skill 规划与设计规范
2.1 一个 Agent 挂多少 Skill 才合理
这是我在设计初期最纠结的问题。挂少了,Agent 能力不够;挂多了,模型在“选择障碍”里迷失,反而什么都不敢调。
实测下来,我的经验是:一个 Agent 日常挂载的 Skill 数量控制在 5 到 8 个之间,超过 10 个之后,模型选错工具的概率会明显上升。你想想,让一个人在二十个工具里挑一个,他都要犹豫一会儿,大模型也一样。工具越多,参数空间越大,出错概率越高。
如果你确实有超过 10 个能力怎么办?两个办法:第一,做“Skill 分组”,比如把监控类、数据处理类、消息通知类各编成一组,Agent 先选组再选具体 Skill;第二,把能力相关的 Skill 合并,比如“查服务器 CPU”“查磁盘使用”“查内存占用”合并成一个“查服务器基础指标”Skill,通过参数区分。
2.2 Skill 的输入输出设计:把不确定变成确定
Skill 和普通函数的区别在于:普通函数的调用方是人,人懂得变通;Skill 的调用方是大模型,它只会照着 Schema 理解参数。所以设计 Skill 时,输入输出必须极度明确,把一切模糊空间堵死。
我整理了一套自己的设计模板,你可以直接参考:
| 设计项 | 要求 | 示例 |
|---|---|---|
| 名称 | 动词开头,目标明确 | query_server_metrics |
| 描述 | 说明用途、适用场景、不适用场景 | 查询指定服务器的 CPU、内存、磁盘使用率。仅用于查询实时指标,不可用于历史数据分析 |
| 输入参数 | 必有默认值、取值范围、单位 | timeout 默认 10 秒,范围 1-60 秒 |
| 输出结构 | 固定字段,扁平化,避免嵌套过深 | 返回 status、data、message 三个字段 |
特别注意:输出结构一定要固定。我在初版 design 的时候,有个 Skill 返回的 JSON 结构不固定,有时候带 error 字段,有时候直接抛异常,导致 Agent 在解析结果时经常猜,一猜就出错。后来把所有 Skill 的输出统一成{ "code": 0, "data": ..., "message": "ok" }这种结构,模型解析的成功率一下子拉满了。
2.3 Agent 记忆与上下文:Skill 之间如何共享状态
设计完单个 Skill 之后,另一个问题是:多个 Skill 之间怎么共享状态。比如用户问“帮我看看北京的服务器怎么样了”,Agent 需要先通过定位 Skill 拿到北京机房的 IP 列表,再把 IP 列表传给巡检 Skill 去查状态。这两次调用是有先后依赖的,后者依赖前者的输出。
处理这个问题的核心策略,就是让 Agent 自己管理中间结果,Skill 尽量保持无状态。换句话说,Skill 不记忆任何东西,只负责基于输入参数执行并返回结果;“北京的 IP 列表是什么”这个状态,由 Agent 的上下文窗口来维护。
这样一来,每个 Skill 都是幂等的、可重试的。同样的输入进去,永远得到同样的输出。这种设计在后端排查问题时特别方便——任何一次异常,你都可以拿着当时的参数重新调用一遍接口,复现问题,而不是靠猜。
3. 腾讯云 AI Skills 落地选型与架构拆解
3.1 为什么选择腾讯云这套生态
说句实话,Skills 的代码逻辑本身并不复杂,有一台服务器就能跑。但真正让它变成“能稳定服务业务”的能力,需要解决部署、弹性扩容、日志、鉴权、域名等一系列工程问题。我自己尝试过自己买服务器折腾,后来还是把整套能力搬到了腾讯云上。
原因有三个。第一是弹性,云函数按调用次数计费,高峰期自动扩容,低峰期不产生费用,不用再为闲置算力买单;第二是生态整合,云函数、API 网关、对象存储、日志服务这些组件都是打通好的,不需要自己拼装;第三是开发调试体验,本地写好代码之后,一条命令推上去,马上就能在线测试。
我见过不少团队花大量时间在自建服务上,最后发现真正花在 Agent 业务逻辑上的时间反而不到一半。如果目标是尽快把 Agent 跑起来、把 Skills 沉淀下来,用托管服务是更现实的选择。
3.2 Skill 的三种部署形态对比
虽然最终我推荐用云函数,但你得先搞清楚不同形态的适用场景,别一上来就选错。我把常用的三种部署形态整理成了一张对比表:
| 形态 | 适用场景 | 优点 | 缺点 |
|---|---|---|---|
| 腾讯云云函数(SCF) | 轻量 API、单次任务、调用不频繁 | 免运维、按量付费、冷启动可接受 | 不适合长时间运行、大内存任务 |
| 容器服务(TKE / 容器镜像)+ 负载均衡 | 重型计算、常驻服务、GPU 推理 | 资源可预估、可控性高 | 成本高、运维复杂 |
| API 网关 + 自建后端 | 已有后端服务,需要快速接入 | 复用现有能力 | 需要自己处理鉴权和限流 |
大多数 Agent 的 Skill 都属于“轻量、低频率、逻辑明确”的类型,所以我建议首选**云函数 **。如果你有某个 Skill 需要跑模型推理或者处理大文件,再单独把它拆成容器服务,通过 HTTP 接口暴露给 Agent 调用。混合架构完全没问题,Agent 的调度层根本不关心后端是什么,只要遵循统一接口协议就行。
3.3 推荐的整体架构:调度层、服务层、存储层
我把整个系统的架构分成三层,每一层的职责边界非常清晰:
- 调度层:负责接收用户问题,调用大模型进行意图识别和工具选择,把用户的自然语言拆解成 Skill 调用计划。这一层我通常不写死逻辑,而是完全依赖模型能力,通过精心设计的 System Prompt 来引导。
- 服务层:由多个独立的 Skill 组成,每个 Skill 是一个云函数,通过 API 网关暴露给调度层调用。这一层只做一件事:输入参数进来,执行逻辑,返回固定结构的结果。
- 存储层:负责存放运行时产生的数据,比如 Agent 的会话历史、Skill 的执行日志、临时文件。我一般用对象存储 COS 存文件,用数据库存结构化的事件记录。
这里有一个关键点:调度层不要直接访问存储层,一切读写都通过 Skill 完成。这样做的好处是,存储的变更不会影响 Agent 主链路,你以后把 COS 换成别的存储,只改对应 Skill 就行。
4. 实操:从 0 到 1 实现一个“日志巡检 Skill”
4.1 场景拆解:从用户需求到 Skill 边界
光讲架构有点飘,我来用一个完整的案例带你走一遍。假设我要做一个“日志巡检”Skill,需求场景是:用户对 Agent 说“帮我看看这几天有没有报错”,Agent 需要去服务器上拉取日志、分析异常关键字、返回一个汇总报告。
第一步不是写代码,而是拆边界。这个需求里有几个子任务:拉日志、过滤关键字、汇总统计、生成报告。我决定把它们都收进一个 Skill 里,因为这是一条连贯的流水线,拆成多个 Skill 反而增加模型编排的负担。
Skill 的输入参数定成三个:start_time(开始时间)、end_time(结束时间)、keywords(一个关键字列表)。输出结果定义为一个 JSON,包含total_logs(日志总数)、matched_logs(匹配数)、top_errors(出现最多的前五个错误)和suggestion(给用户的建议)。
4.2 编写 Skill 代码:函数、Schema、Prompt 模板三段式
我的 Skill 代码永远分成三段来组织。第一段是入口函数,第二段是参数 Schema,第三段是给模型看的描述文本。这里给一个简化版的示例:
# skill_log_audit.py import json from datetime import datetime def run(event, context): """ 云函数入口:这里接入 API 网关的请求事件 """ # 1. 参数校验 params = event.get("queryStringParameters") or {} try: start_time = datetime.fromisoformat(params["start_time"]) end_time = datetime.fromisoformat(params["end_time"]) keywords = json.loads(params.get("keywords", "[]")) except (KeyError, ValueError) as e: return {"code": 400, "data": None, "message": f"参数错误: {e}"} # 2. 核心逻辑(伪代码):拉日志、过滤、统计 raw_logs = fetch_logs(start_time, end_time) matched = [log for log in raw_logs if any(kw in log for kw in keywords)] top_errors = count_and_sort(matched, limit=5) # 3. 返回统一结构 return { "code": 0, "data": { "total_logs": len(raw_logs), "matched_logs": len(matched), "top_errors": top_errors, "suggestion": "建议优先处理出现频率最高的错误,关注生产环境稳定性。" }, "message": "ok" }Schema 部分用 JSON 描述,告诉模型这个 Skill 接受什么参数:
{ "name": "log_audit", "description": "查询指定时间段内的日志,过滤关键字并返回错误统计。当用户询问日志异常、报错情况时使用。", "parameters": { "type": "object", "properties": { "start_time": { "type": "string", "format": "date-time", "description": "开始时间,格式如 2025-01-01T00:00:00" }, "end_time": { "type": "string", "format": "date-time", "description": "结束时间,格式如 2025-01-02T00:00:00" }, "keywords": { "type": "array", "items": { "type": "string" }, "description": "要过滤的关键字列表,如 [\"error\", \"timeout\"]" } }, "required": ["start_time", "end_time", "keywords"] } }这个 Schema 千万别写得太笼统。我第一次写的时候keywords忘了定义items的类型,结果模型传进来一个字符串而不是数组,直接导致解析失败。Schema 写得越死,模型发挥越稳。
4.3 部署上云:云函数加 API 网关完整流程
代码准备好之后,部署到腾讯云上。我用的工具是scf命令行,你也可以用控制台,但命令行适合自动化。
# 登录腾讯云 CLI(初次使用) tccli configure # 创建云函数 tccli scf CreateFunction \ --FunctionName skill_log_audit \ --Handler index.run \ --Runtime Python3.9 \ --Code "ZipFile:$(base64 -w0 skill_log_audit.zip)" \ --Namespace default \ --Role QCS_SCF_QcsRole函数创建好之后,创建一个 API 网关触发器,把 HTTP 请求映射到云函数:
# 创建 API 网关服务 tccli apigateway CreateService --ServiceName skill-gateway --Protocol https # 创建 API,绑定云函数 tccli apigateway CreateApi \ --ServiceId service-xxxx \ --ApiName logAudit \ --Path /log/audit \ --Method GET \ --ServiceType SCF \ --ScfNamespace default \ --ScfFunctionName skill_log_audit到这里,一个可访问的 Skill 接口就生成了。你可以在本地用curl测一下通不通:
curl "https://service-xxxx.coolops.cn/log/audit?start_time=2025-01-01T00:00:00&end_time=2025-01-02T00:00:00&keywords=[\"error\",\"timeout\"]"提示:第一次测试建议直接返回原始结果,不走 Agent。先把 Skill 本身调通,再接 Agent,不要一上来就联调,否则出了错根本分不清是 Skill 的问题还是模型编排的问题。
4.4 在 Agent 中注册 Skill 并做一次完整调用
Skill 部署完成之后,下一步是把它注册到 Agent 的工具列表里。不同 Agent 框架的写法略有不同,但核心都差不多——把上面写的那个 JSON Schema 加到工具的tools参数中。我用的是兼容 OpenAI 工具调用协议的方式:
tools = [ { "type": "function", "function": { "name": "log_audit", "description": "查询指定时间段内的日志,过滤关键字并返回错误统计。当用户询问日志异常、报错情况时使用。", "parameters": { "type": "object", "properties": { "start_time": {"type": "string", "description": "开始时间,格式 2025-01-01T00:00:00"}, "end_time": {"type": "string", "description": "结束时间,格式 2025-01-02T00:00:00"}, "keywords": {"type": "array", "items": {"type": "string"}, "description": "过滤关键字列表"} }, "required": ["start_time", "end_time", "keywords"] } } } ] # 后续正常调用大模型,模型会自行决定是否调用这个工具当 Agent 收到“帮我看看昨天服务的报错情况”这个问题时,模型会在内部判断:应该调用log_audit工具,然后生成一个参数值(start_time 是昨天零点,end_time 是今天零点,keywords 是 [“error”, “exception”]),再发给我们注册的 HTTP API。整个过程,模型扮演的是“调度者”,真正执行的是我们这个云函数。
注册完之后,我强烈建议你跑一遍端到端测试:真实调用 Agent,输入一句口语化的请求,看它是否能够正确选对这个 Skill、传对这些参数。如果传错了,不要急着改 Prompt,先看看是不是 Schema 描述不够清晰——大概率是。
5. 线上运行常见问题与排查实录
5.1 冷启动超时:一次真实故障还原
上线初期,我遇到了一个让人头疼的问题:第一次调用 Skill 时经常超时,但第二次调用就正常了。排查之后发现是云函数的冷启动导致的——函数在长时间没有请求之后,运行时环境被回收,下一次请求需要重新初始化,耗时可能达到几秒钟,而我的 API 网关超时时间设置得太短。
解决办法有几个,按推荐优先级排序:
- 调大 API 网关的超时时间,建议至少 30 秒
- 把数据库连接、HTTP 客户端等重资源的初始化移到全局作用域,避免每次冷启动都重建
- 如果对延迟要求极高,开启云函数的预置并发,牺牲一点成本换稳定性
我自己最终是组合了前两个方案,冷启动从之前的 5 秒降到了 1 秒以内,完全在可接受范围内。
5.2 模型乱传参:用 Schema 把“自由发挥”关进笼子
另外一个高频问题是大模型传参不规范。比如我明明定义了keywords是数组,它却给我传字符串;我定义了start_time是 ISO 格式,它给我传“昨天”。这本质上是模型对 Schema 理解不够造成的,但根因往往是你 Schema 写得不够“死”。
我的建议是:
- 给每个参数加详细的
description,说明格式、单位、取值范围 - 在
required里把必填参数列全,不要给模型自己选择的机会 - 云函数入口做二次校验,后端再兜底一遍,而不是完全信任模型
后端二次校验特别重要。就算模型传了奇怪的参数,你的代码也能返回一个清晰可读的错误,而不是直接抛异常。这样 Agent 能根据错误信息自我纠正,重新生成参数再试一次,这是提高成功率的很有效的手段。
5.3 密钥泄露风险:环境变量和最小权限原则
Skills 里经常需要访问 COS、数据库、内部接口,这就会用到各种密钥。我见过有人在云函数代码里硬编码数据库密码,结果代码仓库一泄露,生产数据库直接暴露在公网。这种低级错误绝对不能犯。
腾讯云上有两个机制来解决这个问题:环境变量和CAM 角色授权。敏感信息一律放环境变量,代码里用os.getenv()读取;如果 Skill 需要访问腾讯云内部资源,尽量用角色而非密钥,让云函数在创建时绑定一个权限受限的 CAM 角色,这样连密钥都不用在代码里出现。
我记得最清楚的一次教训是:一个 Skill 只需要读取 COS 里的某个文件,我却给它配了一个完全读写权限的角色。后来发现这个 Skill 的日志被反复尝试访问其他存储桶,虽然没造成实际损失,但那次之后我再也没给 Skill 赋过超出需求范围的权限。
5.4 Docker 镜像推送失败的三个高频原因
如果你不是用云函数,而是把 Skill 打包成容器镜像部署,那你大概率会遇到 Docker 镜像推送到腾讯云容器镜像服务(TCR)失败的问题。我先后遇到过三种情况,列出来给你避坑:
| 现象 | 原因 | 解决办法 |
|---|---|---|
denied: requested access to the resource is denied | 没有登录镜像仓库,或者登录凭证过期 | 执行docker login重新登录,腾讯云侧用访问令牌方式登录 |
unauthorized: authentication required | 镜像名称和仓库地址对不上 | 检查镜像 tag 是否包含完整的命名空间和仓库名,格式应为ccr.ccs.tencentyun.com/{命名空间}/{仓库名}:{标签} |
manifest invalid | 本地镜像架构和远端不匹配 | 构建镜像时加上--platform linux/amd64,确保和服务器的 CPU 架构一致 |
说到底,推送镜像就和上传文件到网盘一样,登录对了、路径对了、格式对了,基本就成功了。把上面三个问题排查一遍,90% 的推送失败都能解决。
最后再分享一个我个人的体会:写 Agent 最大的成就感,不是模型又学会了什么新话术,而是那些沉淀下来的 Skills 真正变成了“可以被复用的资产”。我用同一套日志巡检 Skill,先是给运维 Agent 用,后来又接进了数据分析 Agent,几乎零成本复用。所以如果你正在做 Agent,别急着追求“全能”,先把你手上最熟悉的那几件事,打磨成靠谱的 Skill,你会发现整个系统的能力会上一个台阶。