1. 别急着造 Agent,先搞清楚你要的是 Agent 还是 Skills
“全能 Agent 养成记”这个标题听起来很唬人,但真正动手做过的朋友都知道,想让一个 Agent 什么都会,最后往往是样样稀松。我在腾讯云上跑了好几个 Agent 项目之后,最大的感触是:“全能”不是靠一个 Agent 塞满技能实现的,而是靠一套设计良好的 Skills 体系,让 Agent 在合适的场景调用合适的能力。
搜索词里反复出现的那些关键词——agent 开发、agent 框架、skill 和 agent 的区别——其实指向的是同一个核心问题:很多人第一步就理解偏了。
1.1 为什么“全能”会变成“全不能”
先说一个我自己的经历。之前接手过一个内部知识库问答 Agent,需求列得很长:读文档、查工单、调数据库、写日报、翻译、代码检查,听起来确实“全能”。我一开始图省事,把所有这些能力全部塞进 system prompt,再加上一个巨大的 tools 列表,让大模型自己选。
结果呢?上下文窗口虽然撑得住,但工具一多,每一步调用工具的准确率肉眼可见地下降。我做了一个不太严谨的统计:tools 列表超过 15 个之后,一个很简单的“查一下本周订单量”请求,偶尔会被路由到完全无关的工具上。这不是模型能力不够,而是你把技能的“编排”和“定义”混在了一起。
正确的做法是分层:一个 Agent 负责理解用户意图,把请求拆解到不同的 Skill 上,而不是让自己同时掌握所有技能。Skill 是“最小可执行能力单元”,有自己的名称、描述、输入输出约定,可以被动态加载和卸载。这套机制跟插件系统非常像——Agent 是宿主,Skills 是插件。
1.2 Skill、Agent、Workflow 的边界到底在哪
很多团队在设计时把“Skill”“Agent”“Workflow”混为一谈,代码里到处都是“Agent 调 Agent”“Skill 里套 Skill”,维护成本直接爆炸。我自己做过一个简单的划分,分享出来供参考:
- Workflow:步骤固定、逻辑确定,比如“收到工单 → 查库存 → 回邮件”,这种场合用传统流程编排就够了,别硬上 Agent。
- Skill:单一能力封装,有清晰的描述和入参出参,比如“查客户余额”“调用翻译接口”。
- Agent:根据用户意图动态决定调用哪些 Skill,以及调用顺序。
如果任务从头到尾路径完全确定,老老实实用 Workflow 就行;只有存在动态决策的需求,才值得引入 Agent。做全能 Agent 的正确姿态,不是“让 Agent 什么都会”,而是在 Agent 上注册一批高质量 Skills,让它在合适的时候调用合适的技能。
1.3 我从零规划的 Skill 清单
下面这张表是我实际用过的技能清单,不是拍脑袋想的,每一项都跑过验证。建议刚起步的团队照着这个维度去设计自己的技能表:
| Skill 名称 | 能力描述 | 类型 | 依赖服务 |
|---|---|---|---|
| extract_doc | 从 PDF/Word 抽取结构化内容 | 文档处理 | 腾讯云 COS |
| memory_search | 在向量库中做语义检索 | 知识检索 | 向量库 |
| db_query | 执行只读 SQL 并返回结果 | 数据查询 | MySQL / 安全网关 |
| notify | 发送企微/钉钉/邮件通知 | 消息推送 | Webhook |
| translate | 多语言翻译 | 通用能力 | 翻译 API |
| code_review | 对代码片段做静态检查 | 技术能力 | LLM 直出 |
这里有一条铁律:每个 Skill 必须做到“可独立测试”,也就是不依赖 Agent 的完整上下文,单测也要能跑通。这个习惯帮我省掉了大量联调时间,后面写代码实现时你会体会到它的价值。
2. 腾讯云底座搭建:服务器选型、域名申请到 Redis 密码坑
确定了 Skills 架构之后,下一步就是在腾讯云上把运行环境搭起来。这一节我完全按实际操作顺序来写,每个环节都标注了容易踩的坑。
2.1 服务器选型不是越贵越好
Agent 服务本身对 CPU 要求不高,真正的消耗在模型调用和向量检索上。如果你把模型部署在云端 API(而不是本地跑开源模型),那么 2 核 4G 的轻量服务器已经足够跑通 demo。但如果是生产级项目,我建议至少 4 核 8G,原因有两点:
- Agent 服务通常用 Python 异步框架,多个并发请求会带来不少内存开销;
- 日志、缓存、模型响应临时文件会持续占用磁盘,存储空间要预留足。
操作系统建议选 Ubuntu 22.04 LTS,Docker、Redis、Nginx 这些组件的兼容性和文档最全。另外磁盘一定要单独挂数据盘,别只挂一个系统盘,日志跑一段时间很容易把根分区吃满。我见过不止一次线上事故是“服务器磁盘满了,服务全部异常”,而根本原因只是日志没做轮转。
2.2 域名申请与二级域名映射
很多教程让你直接用 IP 访问,但实际做 Agent 产品时,回调地址、Webhook、OAuth 跳转都需要域名,而且必须是 HTTPS。腾讯云上可以申请免费 SSL 证书,配合二级域名用起来很方便。
申请步骤很简单,先在域名服务商处添加一条 A 记录,把agent.example.com指向服务器公网 IP,然后在腾讯云 SSL 证书控制台申请免费证书,最后在 Nginx 里配置证书和反向代理。整个过程大概 20 分钟,但有几个细节非常容易忽略:
- 域名解析生效有延迟,配置完别急着测,等一两分钟再做解析验证。
- 免费证书有效期一般三个月,建议写个定时任务自动续期,否则到期后 Agent 接口突然全挂,排查半天才发现是证书过期。
- 安全组记得放行 80 和 443 端口,否则域名能解析但访问不了。
Nginx 反代配置大概长这样:
server { listen 443 ssl; server_name agent.example.com; ssl_certificate /etc/nginx/ssl/agent.example.com.pem; ssl_certificate_key /etc/nginx/ssl/agent.example.com.key; location / { proxy_pass http://127.0.0.1:8000; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } } server { listen 80; server_name agent.example.com; return 301 https://$host$request_uri; }注意:免费证书的自动续期脚本一定要提前测试一遍。我曾经以为定时任务没问题,结果续期那天脚本因为目录权限报错,证书没续上,服务直接挂了一上午。
2.3 安全组别嫌麻烦,按最小权限开
腾讯云控制台的“安全组”是很多新人的第一个坑。默认配置往往是全放通,或者干脆没配,导致 Redis、MySQL 端口直接暴露到公网。
我的建议是:
- 安全组默认拒绝,单独放行 80、443,以及你自己的办公网 IP 的 22 端口。
- 数据库端口(6379、3306)只允许内网访问;如果是跨机器连接,用内网 IP 而不是公网 IP。
- 开发调试时用 SSH 隧道代替直接开放端口。
2.4 Redis 改密码后重启失败:一次完整的排查链路
搜索热词里有一条很典型的问题:“主要是我在腾讯云服务器上安装 redis,但是我修改 redis 密码之后再重启 redis 就一直不”。这个坑我踩过一次,排查过程值得完整写出来。
现象是:改完/etc/redis/redis.conf里的requirepass,执行systemctl restart redis,服务状态显示 failed。看日志journalctl -u redis或/var/log/redis/redis-server.log,常出现的几类原因如下:
- 配置里有两行 requirepass:很多教程会在文件末尾追加
requirepass xxx,但文件里本来就有一行被注释掉的旧配置。当追加行和原有配置形成冲突时,Redis 启动时可能读取到多份密码配置。解决方法是全文搜索requirepass,只保留一行。 - systemd 服务的 User 权限问题:Redis 进程以 redis 用户运行,如果配置目录里某个文件的属主不对,启动时没有权限读取配置,也会直接退出。
- 重启后客户端还在用旧密码连接:服务其实已经起来了,但你的代码连接池没有重新初始化,一直用旧密码重试,表现上就是“重启失败”。直接用
redis-cli -a 新密码 ping手动验证即可。 - protected-mode 和 bind 的组合:改了密码但没改
bind 127.0.0.1,外部连接仍然会被拒绝,看起来像服务没起来。需要确认是否要把 bind 改成内网 IP。
我当时的问题属于第一种。排查顺序建议背下来:先看日志 → 再验端口 → 最后验密码。不要凭感觉去改配置,日志里会直接告诉你原因。
3. 用 LiteLLM Proxy 统一模型网关,这一步省下大量精力
做“全能 Agent”,不可能只接一家大模型。不同模型擅长的任务不一样:翻译、代码、总结、工具调用,各有侧重。但如果每个 Skill 都直接接入各家 API,密钥管理、限流控制、错误重试会让代码从第一天开始混乱。所以我在项目里引入了 LiteLLM Proxy,作为整个 Agent 的模型入口。
3.1 为什么必须有一层网关
你可能觉得:直接调 OpenAI SDK,再给每个模型写一个兼容层不就行了?但问题在于,如果一二十个 Skill 都需要调用模型,每个 Skill 都写一套鉴权和重试逻辑,维护成本非常夸张。而且各家 API 的限流策略、错误码结构、超时时间都不一样,没有统一封装,Agent 跑着跑着就会出现“某个工具莫名其妙失败”的情况。
LiteLLM Proxy 对外暴露一个 OpenAI 兼容的/chat/completions接口,至于背后真实调用哪个模型,全由网关的路由配置决定。对上游 Agent 来说,它只需要认识 OpenAI 的协议格式即可。这就把多模型接入的复杂度收敛到了一层。
3.2 网关配置实战:模型路由与密钥管理
我的配置思路是用一个config.yaml维护所有模型信息,日志、缓存、重试都交给网关。核心结构大概长这样:
model_list: - model_name: gpt-4o-mini litellm_params: model: openai/gpt-4o-mini api_key: os.environ/OPENAI_API_KEY - model_name: deepseek-chat litellm_params: model: deepseek/deepseek-chat api_key: os.environ/DEEPSEEK_API_KEY - model_name: qwen-max litellm_params: model: openai/qwen-max api_base: https://dashscope.aliyuncs.com/compatible-mode/v1 api_key: os.environ/DASHSCOPE_API_KEY litellm_settings: drop_params: true set_verbose: false retry: true num_retries: 3 request_timeout: 30这里model_name是统一入口别名,Agent 代码里只认这个别名,后端模型切换时不用改代码。drop_params很关键,不同模型支持的参数不一样,比如有的模型不支持除temperature之外的高级参数,网关会自动丢弃不支持项,避免报错。
3.3 路由策略与降级方案的实测心得
生产环境里我加了两个规则。
第一个是主备路由:主模型不可用时自动降级到备用模型。LiteLLM 支持在model_list里配置多个同名的model_name,并配合routing_strategy做智能路由。我在实测中发现,按成本路由对控制费用确实有用,但缺点是可能把请求打到一个延迟较高的模型上,所以实际使用中,简单的 fallback 往往比复杂路由更可靠。
第二个是缓存:LiteLLM 支持 Redis 缓存。Agent 场景中很多问题其实是重复的,比如“解释一下库存表结构”,命中缓存后响应速度能压到几十毫秒,费用也省下一大截。不过要注意缓存 key 默认包含 prompt、model 等信息,别把含隐私数据的请求也缓存住,必要时关闭缓存或者做白名单控制。
提示:网关里一定要配置独立的日志输出,把每次请求的模型、token 数、耗时、状态码存下来。后面做成本分析和排障时,这些日志是唯一的依据。
4. AI Skills 核心实现:技能注册表与 Function Calling 范式
这一章讲 Skills 的代码实现。很多人把 Skills 理解为“在代码里写几个函数,然后塞进 tools 参数”,这在 demo 阶段没问题,但项目一复杂就撑不住了。我采用的是“技能注册表 + 动态加载”模式。
4.1 每个 Skill 都是独立的最小单元
在项目里,我的每个 Skill 对应一个类,统一实现同一个接口:
class BaseSkill: name: str description: str parameters: dict # JSON Schema async def run(self, **kwargs) -> Any: ...这样定义的好处是:Agent 可以只通过name、description、parameters三个字段来了解技能,而完全不需要知道技能内部逻辑。就像你请一个助手干活,不需要知道他的每个习惯,只需要知道“他能干什么、需要什么输入、能输出什么”。
实现细节上,我用 pydantic 做参数校验,所有入参在run()之前就会被强类型校验一遍,避免脏数据进到业务逻辑里。这个习惯来自一次真实教训:某个 Skill 收到的日期参数出现了2024-13-40,直接让下游数据库查询报错。
4.2 技能注册表结构与动态加载
技能注册表本质上是一个字典,key 是技能名,value 是 Skill 实例:
skills_registry = { "memory_search": MemorySearchSkill(), "db_query": DbQuerySkill(), "notify": NotifySkill(), }注册动作可以在 Agent 启动时完成,也可以写成装饰器,让每个 Skill 在定义时自动注册:
SKILLS: dict[str, BaseSkill] = {} def register_skill(cls): instance = cls() SKILLS[instance.name] = instance return cls @register_skill class MemorySearchSkill(BaseSkill): name = "memory_search" description = "在知识库中进行语义检索,适合回答公司制度、产品文档等问题" parameters = { "type": "object", "properties": { "query": {"type": "string"}, "top_k": {"type": "integer", "default": 5} } }在注册表之上,每次收到用户请求时,Agent 会先判断意图是否命中某个 Skill。命中就直接调用;不命中就通过 LLM 的 function calling 动态决定。这种“精准命中优先,LLM 兜底”的策略,既保证了稳定性,又保留了泛化能力。
4.3 Function Calling 的上下文管理与记忆取舍
全能 Agent 必然涉及多轮对话,而多轮对话会让 tools 的上下文变得非常长。我的经验是把“记忆”分成两层:
- 短期记忆:当前会话最近几轮的对话内容,随请求一起发送给模型。这个简单直接,但要注意控制轮数和 token 数,我一般保留最近 6 到 10 轮。
- 长期记忆:用 Redis 存会话历史,用向量库存关键结论。比如用户上一轮说过“我们公司在深圳”,后续提到“我们公司”时,Agent 也能借助长期记忆检索到上下文。
从实践来看,长期记忆不是越多越好。见过有项目把上万条历史全塞进 context,结果模型被无关信息干扰,回答质量反而下降。正确姿势是“检索式记忆”:先把历史向量化,需要时用语义相似度检索出最相关的几条,再拼到 context 里。
4.4 实测中的坑:tool 冲突、超时与重试放大
这里有三个运行中才暴露的问题,每一个都值得单独说。
Tool 定义冲突:两个 Skill 的名字都是英文加下划线,但 description 写得太像,模型经常分不清。后来我统一了命名规范,description 必须写清楚“什么场景下使用、和另一个相似 Skill 的区别”。这个改动立竿见影,工具选择的准确率提升明显。
Function Calling 超时:某些 Skill 本身响应很慢,比如外部 API 要 3 秒才返回,但模型端的 function calling 有超时限制。结果表现为 Agent 说“还在处理”,实际上工具调用已经被挂起。解决办法是给 Skill 加超时参数,超时后返回一个明确的{"error": "timeout"},让 Agent 如实告诉用户“查询超时”,而不是永远等待。
重试放大:Agent 调用工具失败时,常见的做法是让 LLM 自己决定重试。但模型有时会执拗地反复调用同一个失败工具,直接翻倍消耗 token。我在网关层加了一个调用频率限制,同一个工具连续失败两次就强制换策略,同时把这个规则写进了 system prompt,让模型知道“不要无限重试同一个失败工具”。
5. 镜像打包与腾讯云容器镜像服务部署实战
代码写完之后,重头戏是部署。标题里的“腾讯云 AI Skills 最佳实践”,很大一部分就体现在这里。部署环节我用 Docker + 腾讯云容器镜像服务 + Docker Compose,把 Agent 服务、LiteLLM Proxy、Redis 编排到一起。
5.1 在容器镜像服务上建好仓库
腾讯云容器镜像服务(TCR)按地域分布,分个人版和企业版,个人版对于学习和小规模项目够用。流程是:登录控制台 → 容器镜像服务 → 创建命名空间 → 建镜像仓库。命名空间建议用项目名,比如agent-project,仓库名对应服务名,比如agent-api。
接着登录镜像服务的专属域名。腾讯云的镜像仓库域名格式通常是ccr.ccs.tencentcloud.com/命名空间/仓库名。登录命令:
docker login ccr.ccs.tencentcloud.com --username=你的腾讯云账号ID密码不是账号密码,而是控制台里生成的访问凭证。这一点很多人会卡住。去“容器镜像服务 - 访问凭证”里新建一个,可以设成长期有效或短期有效。建议用短时凭证,即便泄露,影响范围也能控制住。
5.2 镜像体积和构建规范
Agent 服务用 Python 写的话,基础镜像我推荐python:3.11-slim。先不说安全问题,体积差异就很明显:同一个项目用python:3.11打包完可能超过 1GB,换 slim 后降到 600MB 以内,再配合.dockerignore,通常能压到 300MB 左右。
FROM python:3.11-slim WORKDIR /app COPY requirements.txt . RUN pip install --no-cache-dir -r requirements.txt -i https://pypi.tuna.tsinghua.edu.cn/simple COPY . . EXPOSE 8000 CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]踩过的坑有两个:pip install一定要加--no-cache-dir,否则构建缓存会占大量空间;redis、lxml这类带二进制依赖的包,在 slim 镜像里可能缺系统库,需要额外安装libxml2、libxslt等。如果构建时老报缺依赖,先把这两个装上试试。
5.3 从构建到推送的完整命令链
本地构建、打 tag、推送的命令顺序如下:
docker build -t agent-api:latest . docker tag agent-api:latest ccr.ccs.tencentcloud.com/agent-project/agent-api:latest docker push ccr.ccs.tencentcloud.com/agent-project/agent-api:latestTag 上我建议同时打两个:一个latest,一个具体版本号如v1.2.0。原因是生产回滚需要精确到某个版本,如果你只覆盖latest,回滚时根本不知道上一个可用版本是哪个。
推送完成后,在服务器上拉取镜像:
docker pull ccr.ccs.tencentcloud.com/agent-project/agent-api:v1.2.0服务器如果是新机器,记得先装好 Docker 并设置开机自启,否则重启后容器服务不会自动恢复。
5.4 用 Docker Compose 编排整个 Agent 栈
我的 Compose 文件管理三个服务:agent-api、litellm-proxy、redis。完整内容如下:
services: redis: image: redis:7-alpine restart: always command: redis-server /usr/local/etc/redis/redis.conf volumes: - ./redis/redis.conf:/usr/local/etc/redis/redis.conf - redis_data:/data ports: - "127.0.0.1:6379:6379" litellm-proxy: image: ghcr.io/berriai/litellm:main-latest restart: always environment: - LITELLM_MASTER_KEY=${LITELLM_MASTER_KEY} volumes: - ./litellm-config/config.yaml:/app/config.yaml command: ["--config", "/app/config.yaml", "--port", "4000"] ports: - "127.0.0.1:4000:4000" depends_on: - redis agent-api: image: ccr.ccs.tencentcloud.com/agent-project/agent-api:v1.2.0 restart: always environment: - LITELLM_BASE_URL=http://litellm-proxy:4000 - REDIS_URL=redis://redis:6379/0 ports: - "0.0.0.0:8000:8000" depends_on: - redis - litellm-proxy volumes: redis_data:这里有两个细节特别值得注意。
第一,Redis 端口绑在127.0.0.1而不是0.0.0.0,因为 agent-api 和 litellm 在同一台服务器上,没必要把 Redis 暴露到外网。如果你有跨主机需求,应该通过腾讯云内网 IP 通信,并配置好安全组,而不是直接暴露公网。
第二,restart: always一定要加。Agent 服务挂掉之后,能自动拉起来非常关键。没有这一行,半夜进程崩溃,第二天早上用户反馈全挂,而你只是少了三个单词,这个教训足够深刻。
容器启动之后,验证一下三个服务的健康状态:
docker compose ps curl http://127.0.0.1:4000/health curl http://127.0.0.1:8000/docs如果 Agent 服务暴露了/docs,通常可以看到 FastAPI 自动生成的 Swagger 文档,这一步能快速确认接口是否正常。
6. 上线后的底线工程:日志、权限安全与验收清单
很多项目写到这里就收尾了,但“最佳实践”恰恰体现在上线之后。全能 Agent 比普通 Web 服务复杂得多,它要调模型、调工具、写记忆、处理并发,出问题的点非常分散。所以可观测性和安全必须提前设计,而不是等出了事故再补。
6.1 日志与监控怎么搭才够用
我在 LiteLLM Proxy 那节提过统一日志的重要性。在应用层,我采用结构化日志,每条日志输出为 JSON,包含 request_id、user_id、skill_name、model、token_usage、latency、status。
{ "request_id": "7f3c...", "user_id": "u_1024", "skill_name": "memory_search", "model": "gpt-4o-mini", "token_usage": 1250, "latency_ms": 840, "status": "ok" }这样一个 request_id 就能串起完整链路。配合日志分析平台,可以按 user、按 skill、按 model 维度统计调用量和错误率。这些数据不是拿来好看的,而是拿来回答“为什么昨天费用暴涨”“为什么这个技能成功率这么低”这类问题。
监控告警方面,我配置了三个基础指标:Agent 接口的 5xx 率、平均响应时长、每日 token 消耗。前两个是可用性指标,第三个是成本指标。任何一项超过阈值就告警。成本告警特别有用,因为模型 API 的账单往往是次日才出,等看到账单再处理已经晚了。
6.2 技能权限收敛与输入输出过滤
全能 Agent 最怕的不是模型不够聪明,而是某个 Skill 越权。举个例子:db_query如果被设计成“执行任意 SQL”,后果不堪设想。我在实现时强行做了约束:
db_query只允许SELECT,不允许UPDATE/DELETE,而且强制带 LIMIT。- 对用户输入先做一次脱敏和关键字检查,屏蔽掉明显的注入尝试。
- 每个 Skill 都有独立的访问凭证,而不是共用一个高权限 token。
输入输出过滤方面,我在 Agent 入口做了敏感信息识别,比如身份证号、手机号、密钥等一律打码后再进模型。模型输出同样过一层过滤,避免生成链接或指令被恶意利用。
提示:如果 Agent 要访问外部 API,建议给它单独建一个最小权限账号,不要用主账号 token。即便某个 Skill 的上下文被攻击者注入了恶意指令,影响范围也能被限制在最小权限内。
6.3 上线前我逐项对照的检查清单
最后分享一份我在项目上线前会逐项打勾的清单,不一定适合所有项目,但按这个思路走,能少走一半弯路:
- Agent 的测试环境与生产环境端口是否隔离
- Redis、MySQL 等中间件是否绑定内网或回环地址
- 密钥是否全部使用环境变量或密钥管理服务,而不是硬编码在代码里
- LiteLLM Proxy 是否配置了日志、缓存、重试、超时
- Docker 镜像是否带版本号,
restart: always是否已配置 - 域名证书是否配置了自动续期
- 告警阈值是否已设置并做过一次模拟测试
- 日志是否能通过 request_id 串起完整调用链路
- 所有 Skill 是否都有独立单测和超时控制
这份清单里的每一项,我都在线上踩过对应的坑。比如“密钥硬编码”这条,我第一次部署时把 API key 直接写在代码里,后来被 git 历史翻出来,不得不全部重置。从那之后,凡是涉及密钥,一律走环境变量,开发环境用.env,生产用密钥管理服务。
做一个真正能长稳运行的“全能 Agent”,靠的不是模型有多强,而是外围工程做得有多扎实。Skills 的设计、模型网关的收敛、容器化部署的规范、运维监控的兜底,每一项单独拿出来都不复杂,但组合在一起,才让 Agent 从“能跑 demo”变成“能上生产”。
我自己的体会是:与其追求 Agent 一次把所有事干成,不如先把五六个核心 Skill 打磨到可靠,再逐步扩展。技能在精不在多,稳定性永远排在“全能”前面。后面这个架构能扩展的方向还很多——比如把 Skills 定义成更标准的 OpenAPI 格式、用消息队列做异步任务、把记忆层拆成独立的记忆服务。每一条路都是单独的文章,等我在新项目里跑通了再回来更新。