
1. 为什么我最终选择腾讯云 AI Skills 来搭建 Agent之前有朋友问我Agent 是不是把大模型 API 接进来就算做完了。我说不是只接 API 的那叫聊天机器人Agent 和聊天机器人最大的区别是它能干活而能干活靠的不是模型本身而是它手上握着一组可以调用的技能。上个月我把一个自己维护了大半年的 Agent 项目从手写函数路由整体迁移到了腾讯云 AI Skills 上整个过程走下来最大的感受是技能标准化这件事才是 Agent 从demo 玩具变成生产工具的关键。这篇就围绕这次迁移把从选型到落地的完整实践写清楚给正在做 Agent 开发的团队和个人一个可以直接参考的路线。1.1 裸 function calling 在工具多起来以后的三道坎先说第一版。我最早做 Agent 用的是最常规的 function calling 方案在请求里把工具清单以 JSON Schema 形式喂给模型模型返回一个工具调用意图我的代码根据函数名去分发执行。这套模式跑通第一个 demo 很快两三天就能做出一个能查天气、能算数的玩具 Agent。但等我把业务工具增加到十几个之后问题开始集中爆发。第一道坎是模型选错工具。工具描述稍微写得不够精确模型就会在相似工具之间犹豫。比如我同时有查询用户订单和查询用户退款记录两个工具description 写得接近时模型经常把退款问题路由到订单查询上返回结果自然错得离谱。第二道坎是进程耦合。所有工具函数都编译在 Agent 主进程里一个工具出现内存泄漏或长时间阻塞整个 Agent 跟着遭殃。线上有一次某个工具内部调第三方接口超时我明明只改了一个函数却要重启整个服务这在业务上完全不可接受。第三道坎是观测缺失。每个工具的调用耗时、成功率、参数分布都没有独立统计出了问题只能靠人工翻日志慢慢对时间线效率极低。1.2 AI Skills 解决的不只是调用还有治理后来我认真对比了几条路继续手写函数路由然后自己补监控、用 LangChain 的 Tool 机制做一层封装、或者直接把技能迁到腾讯云 AI Skills 上。最后选了第三个方案原因不是它花哨而是它把技能当成了独立的一等公民来治理。在 AI Skills 这套体系里一个 Skill 不是一个函数而是一个可以被独立描述、独立部署、独立鉴权、独立观测的服务单元。它上报给 Agent 的是标准化的技能说明包括技能名称、功能描述、参数 JSON Schema、回调地址。Agent 需要某个能力时按这个标准协议去调用远端 Skill 服务就行。这个模式带来一个很实际的好处团队协作时算法同学专注调 Agent 的编排策略后端同学专注实现 Skill 内部逻辑两边只需要对齐一份接口描述不需要互相读源码。另外还有一点让我比较放心AI Skills 是云上托管的一套机制这意味着鉴权、调用日志、版本管理这些基础设施是现成的而不是我自己用中间件拼凑。上云这件事在某些场景下被高估但在 Agent 技能管理这个场景下托管的价值非常实在我不需要关心技能接口的网关层怎么做限流不需要自己搭一套密钥管理体系也不用因为流量涨了就去手动改 Nginx 超时。2. 分清 Agent 与 Skill先搭技能体系再谈全能很多做 Agent 的人容易陷入一个误区上来就追求模型多聪明选最强的基座模型写一堆 System Prompt 想让它什么都会。但我的实际经验是Agent 的聪明程度固然和模型有关更大的瓶颈在技能体系的设计上。模型只是大脑大脑再强没有手和脚也干不了活。在腾讯云 AI Skills 的语境下Agent 是调度大脑Skill 是手和脚两者是不同层次的东西设计时必须分开考虑。2.1 Skill 的本质带 Schema 的远程能力单元我习惯把 Skill 理解成一个带插头标准的外接设备。它背后可以是大模型推理也可以是一个普通的 HTTP 服务甚至是一段跑在云函数上的脚本这些都不重要。重要的是它对外暴露的描述足够规范让 Agent 知道你在什么时候该用我、用我的时候要给我什么参数、我会吐给你什么结果。一个标准的 Skill 描述核心字段大概包含这几类字段作用我的备注name技能唯一标识建议小写字母加连字符比如 order-querydescription什么场景下触发这里直接决定模型会不会选错工具值得反复打磨parametersJSON Schema 参数定义每个参数都要写清楚类型、是否必填、取值范围endpoint回调地址Agent 平台会向这个地址发起调用请求auth调用鉴权方式推荐至少使用平台下发的 Token 做请求头校验这里想特别强调一下 JSON Schema 的严格性。模型在参数补全时会严格按照 Schema 来生成 JSON。如果你把参数类型定义成 string但又希望传数字模型的输出可能会带引号Skill 内部如果没做兼容处理就会 500。我在初期踩过这个坑后来所有参数都按最严格的类型和枚举约束来写宁可多写几行 Schema也不让模型自由发挥。2.2 调度逻辑意图识别和参数补全是两件事Agent 侧真正要设计的核心逻辑是把用户说了什么映射到该调哪个 Skill以及参数怎么填。这本质上是两件事但很多初学者混为一谈。意图识别靠的是模型对 Skill description 的理解。模型会阅读所有挂载技能的描述结合用户当前消息判断哪个技能最匹配。这一步决定了 Agent 会不会答非所问。参数补全同样由模型完成它从用户对话里抽取出必要信息填到 Skill 定义好的参数结构里。比如用户说帮我查一下上星期订单量是多少模型要能从这句话里解析出 metricorder_count、start_date 和 end_date 的范围。理解了这两件事是分开的你就能明白为什么 Skill 的 description 那么重要。模型做意图识别时不会真的去看 Skill 内部的代码逻辑它只看你的描述文字。描述写得含糊再强的模型也会犯迷糊。这就像你给一个实习生分配任务指令下得不清不楚就别怪他事办得稀碎。2.3 技能拆分原则按动作拆不要按对象拆技能拆分的颗粒度是最能体现一个 Agent 架构师水平的地方。我一开始犯过一个典型错误按照后端接口的对象来拆技能。比如用户信息服务、订单数据服务、商品信息服务每个技能下面塞了一堆方法。结果模型在意图识别时非常挣扎因为查询用户和查询订单这种对象级的描述在实际对话中边界模糊用户很少说我想调用用户信息服务用户只会说这个客户上次买了啥。后来我把技能按业务动作重新拆了一遍查用户基础信息、查用户最近订单、查商品库存、提交退款申请。每种技能描述的是一个完整的、用户能一句话说清楚的任务。这样调整之后我拿历史问题集做了个小范围评测意图识别的准确率从大概七成提升到了九成以上。当然粒度也不是越细越好。如果一个技能内部需要大量 if-else 分支来处理完全不同的场景说明这个技能拆粗了反过来如果技能数量多到模型在每轮请求里都要浏览上百个描述那不仅会拖慢响应还会增加误选概率。我目前实践下来的阈值是单 Agent 挂载的技能数量控制在 10 到 20 个之间每个技能聚焦一个可以独立验收的业务动作。3. 落地环境准备服务器、镜像仓库、域名一次理清讲完设计和概念接下来是环境准备。这部分内容看起来基础但恰恰是初学者最容易卡住的地方。我自己在腾讯云服务器上折腾 Redis 密码、推镜像、配域名每一步都遇到过大大小小的问题而且这些问题在网上搜到的答案往往残缺不全。这里我把一套完整的落地路径写出来你照着做基本能少走一半弯路。3.1 一台云服务器就够起步配置怎么定先说我目前的部署规格一台 2 核 4G 内存的腾讯云服务器系统选的 Ubuntu。有些朋友会担心配置不够但其实 Agent 的编排层非常轻量它做的事情主要是接收消息、调模型 API、调 Skill 接口、拼装回复真正的重计算发生在两个地方一个是云端的大模型推理另一个是各个 Skill 内部逻辑比如文档向量检索。这两块都不需要跑在 Agent 主进程所在的那台机器上。所以我建议起步阶段不用贪配置。2C4G 跑编排层加两三个轻量 Skill 服务完全够用后续如果某个 Skill 变成热点再单独给那个 Skill 扩容机器。这里有一个容易被忽略的点服务器上一定要装好 Docker并且用 systemd 或 Docker 的 restart 策略来守护 Agent 进程。很多人图省事直接在终端里用 nohup 把进程扔到后台一旦服务器重启进程不会自动拉起来Agent 就悄悄失联了。3.2 搭建过程中的一个经典卡点Redis 改完密码却起不来Redis 在 Agent 项目里的角色很关键会话记忆、缓存数据都靠它。但我发现很多人在改完 Redis 密码后重启服务就起不来了。热词里有人问修改 redis 密码之后再重启 redis 就一直不这个问题我实际踩过而且原因比想象中隐蔽。当时我的操作流程是vim /etc/redis/redis.conf 把 requirepass 改成了新密码然后执行 systemctl restart redis结果服务一直起不来。我下意识以为是密码格式问题反复检查配置文件都没发现异常。最后用 redis-server /etc/redis/redis.conf 在前台启动才看到真正的报错——系统提示无法识别配置文件里的某条指令。根因其实不在 requirepass 本身而是 systemd 的 redis.service 单元文件里写死了 ExecStart 参数。当时安装 Redis 时用的是一条自定义命令ExecStart 里已经用 --requirepass 指定了旧密码。这种情况下systemctl restart 按单元文件的命令启动配置文件里的新密码参数被命令行参数覆盖或冲突服务自然起不来。解决办法很简单打开 /etc/systemd/system/redis.service把 ExecStart 改成只指向 redis-server /etc/redis/redis.conf不附加任何密码参数然后 systemctl daemon-reload 重新加载再正常 start。密码统一收敛到配置文件里维护这是最不容易出错的姿势。顺便提醒一下安全配置如果 Redis 只是给本机的 Agent 服务用bind 请设为 127.0.0.1并且保持 protected-mode yes。不要为了省事把 Redis 暴露到公网否则被扫描工具盯上暴力破解只是时间问题。3.3 用镜像仓库管理 Skill 服务别再用 tar 包传了Skill 服务我推荐用 Docker 镜像来交付。刚开始我也图省事本地 build 完镜像之后 docker save 成 tar 包再 scp 上传到服务器上 docker load。这套流程在只有一台服务器、几乎没有版本变更时问题不大但一旦你需要更新版本、回滚、或者扩展到多台机器tar 包的方式就完全失控了——你根本不知道线上跑的是哪个版本。正确做法是把镜像推到腾讯云容器镜像服务然后在服务器上拉取运行。核心命令就几条# 本地给镜像打标签注意替换成你的镜像仓库地址 docker tag my-agent-skill:latest ccr.ccs.tencentcloud.com/{your_namespace}/agent-skill:latest # 登录镜像仓库 docker login ccr.ccs.tencentcloud.com --username{your_username} --password{your_password} # 推送 docker push ccr.ccs.tencentcloud.com/{your_namespace}/agent-skill:latest镜像仓库的价值不只是存镜像它天然给你提供了版本管理。每次变更打一个新 tag比如 v1.0.0、v1.0.1线上出问题了可以一条命令切回上一个 tag整个过程不会超过 30 秒。服务器上运行时也不要手动 docker run写一个 docker-compose.yml配置好 restart: always、端口映射和资源限制一条 docker compose up -d 搞定所有服务。多服务之间的网络用 Docker 内部网络互通安全性和可维护性都比直接暴露端口好得多。3.4 二级域名和 HTTPS 的必要性Skill 服务要能被 Agent 平台调用必须有一个公网可达、固定的回调地址。国内服务器直接用 IP 加端口有一个问题IP 地址变更或端口被封都会导致服务不可用而且 IP 回调地址在安全和合规上也说不过去。我的做法是在腾讯云上申请了一个二级域名比如 ai.example.com通过 DNS 解析到服务器 IP然后用 Nginx 做反向代理把 443 端口的请求转发到本机的 Skill 服务端口。Nginx 配置可以精简成这样server { listen 443 ssl; server_name ai.example.com; ssl_certificate /etc/nginx/ssl/ai.example.com.crt; ssl_certificate_key /etc/nginx/ssl/ai.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; } }之所以必须上 HTTPS是因为 Agent 平台回调 Skill 时传输的是用户的真实请求数据明文 HTTP 在公网上经过任何一层中间设备都有可能被篡改。你可能觉得这是小概率事件但一旦出现就是大事故。腾讯云上有免费的 SSL 证书可以申请配置过程十几分钟就能完成这笔投入无论如何都值得。4. 从零实现文档问答 业务查询双技能 Agent环境就绪之后就到了动手写代码的阶段。我会用两个实际技能作为例子完整演示一遍一个是本地文档问答技能喂给它产品手册用户提问时它能定位到相关章节另一个是业务数据查询技能从业务数据库或缓存里取聚合指标。这两个技能涵盖了大部分 Agent 项目的典型场景——一个面向非结构化知识一个面向结构化数据。4.1 Skill 一本地文档问答的实现文档问答技能的核心是三点把文档切片并向量化、把用户提问也向量化、在向量库里做相似度检索。我这里用的是轻量方案sentence-transformers 做向量化FAISS 做索引。数据量不大的情况下单机内存就能扛住。from fastapi import FastAPI, Request from sentence_transformers import SentenceTransformer import faiss app FastAPI() model SentenceTransformer(BAAI/bge-small-zh-v1.5) index faiss.read_index(./docs.index) app.post(/v1/skills/docqa) async def docqa(req: Request): payload await req.json() question payload.get(question, ) if not question: return {code: 400, msg: question is required, data: []} vec model.encode([question], normalize_embeddingsTrue) scores, idx index.search(vec, k3) return { code: 0, msg: ok, data: [ {doc_id: int(i), score: float(s)} for i, s in zip(idx[0], scores[0]) ], }这个接口返回的是文档片段 ID 和相似度分数Agent 拿到后会把对应片段内容取出来作为上下文继续生成回答。这里有一个设计细节向量检索只是召回真正的回答生成还是由大模型完成Skill 内部尽量不要自己拼装自然语言因为自然语言生成不是技能该管的事技能应该返回结构化结果。4.2 Skill 二业务数据查询的防坑设计第二个技能是业务数据查询。这个技能最容易犯的错误是把数据库直接暴露给 Agent让模型自己写 SQL。风险显而易见模型生成的 SQL 可能包含高危操作也可能因为表结构复杂而生成错误查询。正确做法是做一个参数白名单化的聚合接口。app.post(/v1/skills/metric-query) async def metric_query(req: Request): payload await req.json() metric payload.get(metric, ) start_date payload.get(start_date, ) end_date payload.get(end_date, ) allowed_metrics {order_count, order_amount, refund_count} if metric not in allowed_metrics: return {code: 400, msg: fmetric not allowed, must be one of {allowed_metrics}} # 按 metric 映射到具体的查询逻辑而不是拼接 SQL result query_daily_agg(metric, start_date, end_date) return {code: 0, data: result}用户在对话里说查一下最近的营收模型会把 metric 参数填成 order_amount把时间范围填成最近的日期。Skill 内部只接受白名单里的指标名查询逻辑也按指标名走不同的聚合函数不接收任意 SQL。这样即使模型的参数生成有偏差最坏情况是返回一个参数不支持的提示不会把底层表结构暴露出去。4.3 Agent 编排层的骨架代码两个 Skill 就绪后Agent 编排层反而是最简单的。它的核心逻辑是接收用户消息带着技能清单调一次模型如果模型决定调用某个 Skill就发 HTTP 请求到回调地址把返回结果再次交给模型汇总成自然语言。用伪代码表达就是这样一个流程user_input receive_message() session_history redis.get(session_id) # 模型在这里会同时做意图识别和参数补全 response llm.chat( messagesbuild_messages(user_input, session_history), toolsattached_skills ) if response.tool_call: skill_result http_post( urlresponse.tool_call.endpoint, argsresponse.tool_call.arguments ) final_answer llm.chat(original_messages skill_result) else: final_answer response.content redis.set(session_id, updated_history, ttl86400)这里要注意的是不要把完整技能描述塞进 messages 里手动拼直接用平台提供的技能挂载机制。你只要在平台上把 Skill 挂到 Agent 名下平台会在每次模型请求时自动注入技能清单省去自己维护工具列表的麻烦也避免工具描述更新后 Agent 侧不同步的问题。4.4 会话记忆为什么放在 Redis多轮对话能力是 Agent全能感的重要来源。用户不可能每句话都把背景交代完整他可能上一句问上个月订单量多少下一句问那退款呢。这里的那退款呢依赖上文的指标对象如果 Agent 没有记忆模型根本不知道用户在问什么。我的方案是用 Redis 按 session_id 保存最近 N 轮对话的原始消息和系统生成的上下文摘要。key 的格式类似 agent:{agent_id}:session:{session_id}value 用 JSON 序列化存消息数组同时设置 TTL一般 24 小时过期。Redis 的过期机制天然适合会话场景不需要自己写定时清理任务。需要注意给 Redis 设置合理的 maxmemory 和淘汰策略否则会话多了内存会持续上涨。多用 incr 和 expire 这类原子操作来控制请求频率避免单个 session 刷爆内存。5. 从能用到好用技能描述、超时与观测调优Agent 跑通 demo 不难难的是把准确率和稳定性调到可以放心交给用户的程度。这一节我梳理了几个投入产出比最高的调优点都是我在实际项目中反复验证过的。5.1 Skill 描述是性价比最高的 Prompt 工程同样是接一个订单查询技能description 写得不同效果可能差出一倍。我见过很多人写 description 就一行字查询订单信息。模型面对这样的描述根本不知道什么时候该触发它。用户问我买的那个快递到哪了模型就无法把这句话和查询订单信息关联起来。我现在的写法会刻意包含四个要素触发场景、输入要求、输出说明、兜底默认值。对比一下就清楚# 差的写法 查询订单信息 # 好一点的写法 当用户询问订单状态、物流进度、订单金额等与订单相关的问题时使用。 输入参数包括订单号或用户ID输出为订单的当前状态、金额、创建时间。 如果用户未提供订单号默认查询该用户最近一笔订单。模型在意图识别时就是靠这段描述做语义匹配的。描述里出现过的词用户在实际对话中更容易被模型对应上。如果你发现某个技能经常被漏选第一步不是换模型而是把 description 里的触发词补充完整然后观察效果变化。5.2 超时、重试与幂等稳定性的基本盘Skill 调用是跨网络的网络波动、下游服务变慢都是常态。如果 Agent 编排层不给 Skill 调用设超时一个慢接口会把整轮对话拖死。我在代码里给每个 Skill 调用都设了 5 秒超时超过就返回一个明确的超时错误给模型让模型对用户说服务暂时繁忙而不是让用户无限转圈。重试策略要克制。跨网络调用重试经常是雪上加霜尤其当下游已经过载时你的重试只会加重对方压力。我的做法是只重试一次且使用指数退避第一次失败后等 1 秒再试如果第二次还失败就不再重试。另外Skill 内部要保证接口幂等也就是同一个请求执行多次结果一致。查询类接口天然幂等提交类接口需要在入参里加请求 IDSkill 侧对相同请求 ID 只处理一次避免用户点了两次提交产生两笔订单。5.3 日志回放是 Agent 排障的唯一可靠手段Agent 项目的排障难度比普通后端高一个量级因为一次错误的回答可能是模型理解偏差、技能描述冲突、参数填充错误、下游接口异常等多种原因叠加的结果。这种情况下没有日志回放能力基本就是在黑箱里猜。我要求所有 Skill 接口和 Agent 编排层都输出结构化 JSON 日志字段包括请求时间、用户问题、命中的 Skill、模型的原始调用参数、Skill 的实际响应、单次调用耗时、最终回复内容。每一条日志都带 trace_id把整个调用链路串起来。腾讯云上的日志服务是一个很好的落点编排层和各个 Skill 服务的日志都采集到同一个日志主题下按 trace_id 搜索就能完整还原一次用户请求的全过程。有一次用户反馈某天下午 Agent 频繁报错我通过日志发现是某个 Skill 的 p95 耗时从 200ms 飙升到 2s进一步定位到是数据库一条慢查询整个过程不到半小时这在没有日志系统之前是不可想象的。5.4 独立部署带来的扩缩容优势最后聊一下扩容。Skill 服务独立部署之后你可以针对性的扩容而不是把整个 Agent 拿来一起扛。比如某个热门技能突然被大量调用只需要给这个 Skill 服务加副本编排层保持轻量无状态会话状态都在 Redis整个架构的伸缩性非常清晰。这就需要你在设计时保证 Agent 编排层是无状态的。除了 Redis 里的会话数据编排进程里不要保存任何本地状态这样任何一台服务器上的编排实例都可以处理任何用户的请求。线上流量翻倍时我直接把编排层从 1 个副本扩到 3 个副本再给热点 Skill 单独加副本整个过程不需要改一行代码只是调整了部署配置。6. Agent 突然听不懂了一次完整排障链路复盘最后分享一次真实的线上排障经历。某天上午我陆续收到用户反馈说 Agent 开始答非所问同一个问题之前回答正常突然就变得牛头不对马嘴。第一反应是模型服务出问题了但查了状态发现模型侧完全正常于是开始一层层定位自己的链路。6.1 先看日志回放不要急着改代码我打开日志系统按用户反馈的时间段拉出几条完整链路。发现一个规律这些出错的请求全部命中了一个错误技能。用户问的是售后政策系统却调用了订单查询技能返回了一堆订单号模型拿这些无关数据硬编出了回答。到这里基本可以确定问题出在意图识别环节而不是模型本身。接着我对比了出问题前后的配置变更记录。发现前一天晚上我更新过一个 Skill 的版本新版本把原来名称为 refund-policy 的技能改成了 return-policy同时改了回调地址。问题就出在这里——Skill 服务侧的代码已经更新了但平台侧挂载的技能描述没有同步刷新模型看到的还是旧的技能清单描述和实际行为不匹配。6.2 根因是元信息不同步这个坑很多人会踩很多 Skill 更新事故的根因不是代码写错而是代码已经变了但 Agent 侧看到的技能描述还是旧的。Skill 不是一个纯后端服务它同时包含运行时逻辑和给模型看的元信息两部分。改代码只是改了一半还要让 Agent 平台重新同步技能描述。如果平台有缓存或异步同步机制更新后需要主动触发刷新并等待生效。我在那次事故里就是更新完代码后没有做同步验证导致线上已经切到了新版本但模型还在按旧描述做意图识别。这次事故之后我把技能上线的流程固定成了四步改代码并推送镜像、更新平台上的技能描述、主动触发同步并确认生效、跑一轮冒烟用例。冒烟用例是一个固定的问题集覆盖每个技能的核心触发场景。每次上线后自动跑一遍确认所有技能的路由和参数填充都正常再宣告上线完成。6.3 兜底设计当用户的问题不属于任何技能这次排查还让我意识到另一个问题Agent 不是变笨了而是它根本没有定义当用户问的内容不属于任何技能时该怎么办。模型在意图识别时如果找不到匹配的技能可能会硬选一个最接近的结果就是答非所问。我后来在编排层加了一条兜底规则如果模型判断不需要调用任何 Skill就直接告诉用户这个问题我暂时无法处理而不是强行生成一个回答。这个兜底看似简单但对用户体验的提升非常明显——用户至少知道 Agent 的能力边界在哪里不会觉得它在一本正经地胡说八道。真正优秀的 Agent不是假装自己无所不能而是清楚地知道什么能做、什么不能做然后把能做的部分做到极致。我在这个项目里最大的体会是Agent 的能力天花板从来不是模型决定的而是技能体系决定的。一个全能 Agent的养成本质上是一套 Skill 体系的持续演进。腾讯云 AI Skills 这个方案的价值在于它把技能做成了标准化、可观测、可独立部署的单元让团队在加技能时不需要反复动编排内核。如果你也在做 Agent我的建议是先别追求大而全把两三个核心 Skill 做扎实跑通一个完整闭环再逐步扩展技能面。技能体系稳了Agent 的全能感会自然长出来。