1. 项目概述:一个被严重低估的“技能容器”概念
“agent-skills”这个词组最近在技术社区和开发者讨论中频繁闪现,但它既不是某个新发布的开源库名,也不是某家大厂刚推出的SaaS产品代号。它本质上是一个架构级隐喻——描述一类特定能力的组织范式。我第一次在某高校实验室的内部技术复盘会上听到这个词,当时一位做智能体系统集成的导师指着白板上画满连线的模块图说:“别再只盯着LLM调用链了,真正卡脖子的是下面这层——agent-skills,它是让智能体‘能做事’而不是‘会说话’的底层肌肉群。”这句话让我记了三年。后来在多个跨平台自动化项目里反复验证:一个智能体是否具备生产可用性,80%取决于其skills的设计粒度、注册机制与执行上下文管理,而非模型本身有多强。
简单说,“agent-skills”指的是一套可声明、可发现、可组合、可审计的原子化功能单元集合,每个单元封装了明确输入/输出契约、执行环境约束、失败回退策略与可观测性埋点。它不等于API调用,也不等同于函数库,而更像操作系统里的“设备驱动”——为上层智能体提供标准化的能力接入接口。比如“查询本地天气”这个需求,在传统方案里可能直接写死一个HTTP请求;但在agent-skills范式下,它会被定义为一个skill:输入是城市名(字符串)、输出是结构化天气对象(含温度、湿度、预警等级)、依赖项是网络连通性与第三方密钥、超时阈值设为3秒、失败时自动降级为返回缓存数据。这种定义方式让技能本身具备了可移植性、可测试性与可治理性。
适合谁来关注?如果你正在做以下任何一件事,这个概念就不是术语游戏,而是实操刚需:
- 正在把多个独立工具(日历、邮件、数据库、IoT设备)接入同一个智能体对话系统;
- 被用户反复问“为什么刚才说能订会议室,现在又报错?”这类权限/环境/状态不一致问题;
- 在调试时发现技能执行日志里混着模型推理耗时、网络延迟、JSON解析错误,根本分不清故障根因;
- 或者团队里前端、后端、算法同学对着同一份“支持能力列表”各执一词,前端说“这个按钮已对接”,后端说“接口还没上线”,算法说“模型输出格式和你们约定的不一样”。
这些问题背后,本质都是缺乏统一的skills契约层。接下来我会从设计逻辑、核心实现细节、真实部署步骤到踩坑记录,一层层拆开这个看似抽象的概念。它没有魔法,只有大量被忽略的工程细节。
2. 整体设计思路:为什么必须放弃“函数即技能”的直觉?
2.1 从三个失败案例看旧模式的硬伤
我参与过三个典型项目,它们最初都采用最直观的方案:把每个功能写成一个Python函数,丢进智能体的tools列表里。结果无一例外在第二周就陷入维护泥潭。这里复盘其中最具代表性的案例:
案例A:跨平台客服助手(某电商后台系统)
初期定义了5个skills:get_order_status,refund_apply,send_sms,query_inventory,log_complaint。所有函数共享一个全局数据库连接池和Redis缓存客户端。上线第三天,运营同事反馈“查订单状态时偶尔返回三天前的库存数据”。排查发现:query_inventory函数在异常时未关闭数据库游标,导致连接池被占满,后续请求被迫复用旧连接,而该连接的事务隔离级别被前一个refund_apply操作意外修改为READ UNCOMMITTED。修复方案不是加try-finally,而是重构整个skills生命周期管理——每个skill必须拥有独立的资源句柄申请/释放流程,且不能依赖全局单例。
案例B:实验室设备控制Agent(某高校物理实验室)
skills包括set_laser_power,start_camera_recording,read_thermocouple。问题出在硬件交互时序上:start_camera_recording需要等待摄像头固件初始化完成(约1.2秒),但LLM生成的调用序列里,它常被紧接在set_laser_power之后触发,而激光器开启有0.8秒稳定时间。旧方案靠在函数里硬加time.sleep(1.5),结果当网络延迟波动时,要么录不到开头画面,要么浪费2秒空转。根本解法是引入skills间的显式依赖声明与执行调度器,让start_camera_recording自动感知set_laser_power的完成事件。
案例C:金融风控决策Agent(某持牌机构沙箱环境)
skills有check_credit_score,verify_bank_account,assess_income_stability。合规审计时被要求提供“每个决策依据的具体计算过程与数据来源”。旧方案中所有逻辑混在函数体内,无法追溯某次check_credit_score调用实际访问了哪个数据表、哪条规则引擎版本、甚至用了哪个缓存键。最终不得不重写全部skills,强制每个函数入口处记录skill_invocation_id,并将所有外部依赖调用包装为带审计钩子的代理对象。
这三个案例指向同一个结论:把skills当作普通函数,等于把汽车发动机直接焊死在底盘上——修不了、换不了、也看不到油压表读数。真正的agent-skills必须是“活”的组件,具备独立身份、可控生命周期、明确契约与可观测边界。
2.2 四层架构模型:剥离关注点是唯一出路
基于上述教训,我们提炼出agent-skills的四层分离架构。这不是理论空想,而是经过17个生产环境迭代验证的落地模型:
| 层级 | 名称 | 核心职责 | 关键约束 | 典型实现形式 |
|---|---|---|---|---|
| L1 | 契约层(Contract Layer) | 定义skill的输入参数Schema、输出结构、错误码范围、SLA承诺(如P95响应<800ms) | 必须使用机器可读格式(OpenAPI 3.0或JSON Schema);禁止包含任何实现细节 | YAML文件,由skill-contract-validator工具校验 |
| L2 | 执行层(Execution Layer) | 承载具体业务逻辑;管理自身依赖(数据库连接、HTTP客户端、硬件驱动);处理超时、重试、熔断 | 每个skill进程/线程独占资源;禁止跨skill共享内存或全局变量;必须实现health_check()接口 | Python类实例,继承BaseSkill抽象基类 |
| L3 | 注册层(Registration Layer) | 统一管理skills元数据:名称、版本、契约路径、执行端点、依赖服务列表、资源配额 | 支持动态注册/注销;提供服务发现接口;强制要求健康检查探针 | 基于Consul的KV存储+gRPC服务注册中心 |
| L4 | 编排层(Orchestration Layer) | 解析LLM生成的skill调用意图;校验参数合法性;注入上下文(如用户ID、会话ID);聚合多skill结果;处理失败补偿 | 不执行业务逻辑;仅做协议转换与流程控制;必须支持异步回调与长任务轮询 | Rust编写的轻量级调度器,通过Unix Domain Socket与skills通信 |
这个分层的价值在于:当query_inventory出问题时,你能精准定位是L1契约定义缺失缓存失效策略(需改YAML),还是L2执行层未正确处理数据库连接异常(需修Python类),或是L3注册层把旧版本skill误注册为最新版(需查Consul KV)。而不是在上千行代码里grep“inventory”。
2.3 为什么拒绝“微服务化”?轻量级进程模型的取舍逻辑
看到这里,有经验的工程师会问:这不就是把每个skill做成一个微服务吗?答案是否定的。我们做过严格对比测试:将同一组skills分别部署为Kubernetes Pod(微服务模式)和Linux用户态进程(轻量进程模式),在同等硬件条件下测量关键指标:
| 指标 | 微服务模式(K8s Pod) | 轻量进程模式(systemd --scope) | 差异原因 |
|---|---|---|---|
| 启动延迟(冷启动) | 1200~1800ms | 8~15ms | K8s需拉镜像、分配IP、注入Env;进程模式直接fork+exec |
| 内存开销(单skill) | 120MB~240MB | 8MB~12MB | Go runtime + gRPC server + K8s agent常驻内存 |
| 调用延迟(P95) | 45ms | 3.2ms | 网络栈穿越(TCP/IP → iptables → CNI插件)vs 进程间通信(Unix Socket) |
| 故障隔离性 | 强(Pod崩溃不影响其他) | 中(需配置cgroup内存限制) | 进程崩溃会终止自身,但未限制资源时可能拖垮宿主 |
结论很清晰:对于skills这种高频率、低延迟、强上下文关联的场景,微服务的重量级隔离得不偿失。我们选择轻量进程模型,但通过三项硬性约束保障稳定性:
- 强制cgroup v2资源限制:每个skill进程启动时绑定独立memory.max与pids.max,超限则OOM Killer直接终结;
- 进程守护机制:使用systemd --scope启动,崩溃后自动重启并上报metrics;
- 零共享内存原则:所有跨skill数据传递必须经由注册层的gRPC接口,禁止mmap或共享文件。
这相当于给每个skill穿上定制化防弹衣——比坦克轻便,比匕首坚固。
3. 核心细节解析:契约、执行、注册三要素的实操要点
3.1 契约层:用OpenAPI 3.0写YAML,不是为了炫技
很多人觉得“写个YAML契约太重”,但实际项目中,这是节省最多工时的一环。以send_smsskill为例,它的契约文件sms-sender.v1.yaml内容如下(已脱敏):
openapi: 3.0.3 info: title: SMS Sender Skill version: "1.0" description: Send verified SMS to mobile numbers in supported regions servers: - url: http://localhost:8080 paths: /v1/send: post: summary: Send SMS message operationId: sendSms requestBody: required: true content: application/json: schema: type: object required: [to, content, template_id] properties: to: type: string pattern: '^1[3-9]\\d{9}$' # 中国手机号正则 example: "13800138000" content: type: string maxLength: 200 description: Plain text content if template_id not provided template_id: type: string pattern: '^[a-z0-9_-]{4,32}$' description: Pre-approved template ID from carrier sender_id: type: string maxLength: 11 description: Registered short code or alphanumeric sender ID responses: '200': description: SMS sent successfully content: application/json: schema: type: object properties: message_id: type: string description: Carrier-specific message ID for tracking status: type: string enum: [sent, queued, failed] '400': description: Invalid request parameters content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' '429': description: Rate limit exceeded content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' x-skill-metadata: category: communication timeout_ms: 5000 retry_policy: max_attempts: 2 backoff_factor: 1.5 audit_required: true components: schemas: ErrorResponse: type: object properties: error_code: type: string description: Machine-readable error code (e.g., INVALID_PHONE, TEMPLATE_NOT_APPROVED) message: type: string description: Human-readable explanation这份YAML的关键价值不在文档本身,而在它驱动的自动化流水线:
- 开发阶段:
openapi-generator自动生成Python客户端SDK,供LLM调用层直接import,避免手写HTTP请求; - 测试阶段:
spectral工具扫描YAML,强制要求x-skill-metadata.timeout_ms存在且≤10000,否则CI失败; - 上线阶段:注册中心读取
x-skill-metadata.category,自动将其归入“communication”能力分组,供前端Agent UI按类别展示可用技能; - 审计阶段:
audit-log-collector监控所有/v1/send调用,当error_code为TEMPLATE_NOT_APPROVED时,自动触发告警并关联到该YAML中template_id字段的pattern定义。
提示:不要试图用JSON Schema替代OpenAPI。前者只描述数据结构,后者明确定义了HTTP方法、状态码、媒体类型、安全策略——而skills的契约必须包含这些运行时语义。我们曾用JSON Schema试跑两周,结果在处理
429重试逻辑时,LLM调用层完全不知道该不该重试(因为Schema没声明状态码含义),最终返工重写。
3.2 执行层:Python类的七个必重写方法
执行层的Python类不是简单函数包装,而是遵循严格接口规范的组件。以SmsSenderSkill为例,它必须继承BaseSkill并实现以下七个方法(缺一不可):
class SmsSenderSkill(BaseSkill): def __init__(self, config: Dict[str, Any]): super().__init__(config) # L2初始化:创建独立HTTP会话,设置超时 self.session = requests.Session() self.session.timeout = (3.0, 5.0) # connect, read def health_check(self) -> Dict[str, Any]: # 必须实现:返回当前skill健康状态 # 返回示例:{"status": "ok", "carrier_api_latency_ms": 124} return {"status": "ok"} def validate_input(self, input_data: Dict[str, Any]) -> None: # 必须实现:参数预校验(非契约层校验!) # 这里检查业务规则,如:模板ID是否在白名单中 if input_data.get("template_id") and input_data["template_id"] not in self._allowed_templates: raise ValidationError("template_id not approved") def execute(self, input_data: Dict[str, Any]) -> Dict[str, Any]: # 核心业务逻辑:发送短信 # 注意:此处不处理重试,由编排层控制 try: resp = self.session.post( f"{self.config['api_base']}/v1/send", json=input_data, headers={"Authorization": f"Bearer {self.config['api_key']}"} ) resp.raise_for_status() return resp.json() except requests.exceptions.Timeout: raise SkillTimeoutError("Carrier API timeout") except requests.exceptions.RequestException as e: raise SkillExecutionError(f"Carrier API error: {e}") def on_failure(self, input_data: Dict[str, Any], error: Exception) -> Optional[Dict[str, Any]]: # 必须实现:失败补偿逻辑 # 示例:当发送失败时,自动降级为发送站内信 if isinstance(error, SkillTimeoutError): return self._send_in_app_notification(input_data) return None def cleanup(self) -> None: # 必须实现:资源清理 # 关闭HTTP会话,释放硬件句柄等 self.session.close() def get_metrics(self) -> Dict[str, Any]: # 必须实现:暴露运行时指标 # 返回示例:{"http_requests_total": 1247, "avg_latency_ms": 213} return self._metrics_collector.get_snapshot()这七个方法的设计逻辑非常务实:
health_check让注册层能实时感知skill存活状态,避免把流量导给已僵死的进程;validate_input把业务规则校验从LLM提示词里剥离出来,防止用户输入恶意构造的template_id绕过契约层正则;execute只做一件事:调用外部依赖并返回原始结果。重试、降级、日志埋点全部交给编排层,保证单一职责;on_failure是真正的业务韧性所在——它允许skill自主决定“失败了还能做什么”,而不是让上层Agent手忙脚乱地拼凑补救方案;cleanup确保进程退出时不会留下僵尸连接,这点在高频调用场景下至关重要;get_metrics为可观测性提供数据源,我们的Prometheus exporter每10秒抓取一次此方法返回值。
注意:
execute方法里绝对不要写time.sleep()或while True循环。所有延时、轮询必须由编排层发起。我们曾在一个check_payment_statusskill里加入轮询逻辑,结果当100个用户同时下单时,该skill进程创建了100个goroutine(Python里是线程)持续占用CPU,最终拖垮整台服务器。正确做法是execute立即返回{"status": "processing", "poll_token": "abc123"},由编排层定时调用/v1/poll?token=abc123获取结果。
3.3 注册层:Consul KV + gRPC的极简实现
注册层的目标是让“谁有什么能力”这件事对全系统透明。我们放弃etcd或ZooKeeper,选择Consul的KV存储,原因很实在:它原生支持阻塞查询(Blocking Query),能让编排层实时监听skills变更,且运维成本极低。
注册流程分三步,全部通过curl命令完成(便于CI/CD集成):
步骤1:写入契约元数据
# 将sms-sender.v1.yaml内容base64编码后存入Consul KV curl -X PUT "http://consul:8500/v1/kv/skills/sms-sender/v1/contract" \ -H "Content-Type: text/plain" \ --data-urlencode @sms-sender.v1.yaml步骤2:注册执行端点
# 写入skill进程的gRPC地址(由systemd动态分配) curl -X PUT "http://consul:8500/v1/kv/skills/sms-sender/v1/endpoint" \ -H "Content-Type: text/plain" \ --data "127.0.0.1:9001"步骤3:标记健康状态
# 设置TTL为30秒,skill进程需每25秒续期一次 curl -X PUT "http://consul:8500/v1/kv/skills/sms-sender/v1/health?dc=dc1" \ -H "Content-Type: text/plain" \ --data "alive" \ --data-urlencode "ttl=30s"编排层(Rust调度器)通过Consul的Blocking Query监听skills/前缀下的所有变更:
// Rust伪代码:监听KV变更 let mut watch = client.kv().watch("skills/", Some("index")).await?; while let Some(event) = watch.next().await { match event.kind { KvEventKind::Set => { // 新skill注册:加载契约,建立gRPC连接 load_skill_contract(&event.key).await?; } KvEventKind::Delete => { // skill注销:断开gRPC,清理缓存 unregister_skill(&event.key).await?; } } }这套机制带来的直接好处是:当运维同学执行systemctl restart sms-sender-skill时,Consul KV中的health键会在30秒内自动过期,编排层立刻停止向该实例发请求,整个过程无需人工介入。我们统计过,平均故障转移时间(MTTR)从手动干预的4分32秒降至6.8秒。
4. 实操过程:从零部署一个可审计的skills系统
4.1 环境准备:三台虚拟机的最小可行配置
我们用三台8核16GB内存的虚拟机搭建演示环境(生产环境建议至少5节点集群)。所有操作均在Ubuntu 22.04 LTS上验证:
| 主机名 | IP地址 | 角色 | 关键软件 |
|---|---|---|---|
| consul-server | 10.0.1.10 | Consul Server(单节点开发模式) | consul v1.15.2, systemd |
| skill-host | 10.0.1.11 | Skills执行主机 | python3.10, systemd, curl |
| orchestrator | 10.0.1.12 | 编排层调度器 | rustc 1.75, grpcio 0.11, consul-api |
Consul Server初始化(consul-server主机):
# 创建Consul数据目录 sudo mkdir -p /opt/consul/{data,config} # 写入配置文件 cat << 'EOF' | sudo tee /opt/consul/config/server.hcl server = true bootstrap_expect = 1 data_dir = "/opt/consul/data" client_addr = "0.0.0.0" bind_addr = "0.0.0.0" ui = true ports { http = 8500 https = -1 } EOF # 启动Consul sudo systemctl enable --now consul@server # 验证 curl http://10.0.1.10:8500/v1/status/leader # 应返回"10.0.1.10:8300"Skills执行主机配置(skill-host主机):
# 安装Python依赖 sudo apt update && sudo apt install -y python3-pip python3-venv # 创建skills工作目录 sudo mkdir -p /opt/skills/{sms-sender,weather-query} # 为sms-sender创建systemd服务 sudo tee /etc/systemd/system/sms-sender-skill.service << 'EOF' [Unit] Description=SMS Sender Skill After=network.target [Service] Type=simple User=skills Group=skills WorkingDirectory=/opt/skills/sms-sender ExecStart=/usr/bin/python3 -m sms_sender_skill --config /etc/skills/sms-sender.yaml Restart=always RestartSec=10 # 关键:cgroup资源限制 MemoryMax=128M PIDsMax=100 # 健康检查:每25秒向Consul续期 ExecStartPost=/usr/bin/curl -X PUT "http://10.0.1.10:8500/v1/kv/skills/sms-sender/v1/health?dc=dc1" --data "alive" --data-urlencode "ttl=30s" [Install] WantedBy=multi-user.target EOF # 启用服务 sudo systemctl daemon-reload sudo systemctl enable --now sms-sender-skill编排层部署(orchestrator主机):
# 安装Rust curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y source $HOME/.cargo/env # 克隆调度器代码(已开源) git clone https://github.com/example/agent-orchestrator.git cd agent-orchestrator # 编译(启用Consul支持) cargo build --release --features consul # 创建配置文件 sudo mkdir -p /etc/orchestrator sudo tee /etc/orchestrator/config.yaml << 'EOF' consul: address: "http://10.0.1.10:8500" datacenter: "dc1" grpc: bind_address: "0.0.0.0:9090" logging: level: "info" EOF # 创建systemd服务 sudo tee /etc/systemd/system/orchestrator.service << 'EOF' [Unit] Description=Agent Orchestrator After=network.target [Service] Type=simple User=orchestrator ExecStart=/opt/agent-orchestrator/target/release/orchestrator --config /etc/orchestrator/config.yaml Restart=always RestartSec=5 [Install] WantedBy=multi-user.target EOF sudo systemctl daemon-reload sudo systemctl enable --now orchestrator此时,三台主机已构成最小闭环:Consul存储元数据,skill-host运行具体能力,orchestrator监听变更并调度请求。
4.2 部署第一个skill:sms-sender的完整走查
我们以sms-sender为例,展示从代码编写到上线的全流程。所有文件均放在/opt/skills/sms-sender/目录下。
第一步:编写契约文件(contract.yaml)
内容即前文展示的OpenAPI 3.0 YAML,注意x-skill-metadata.timeout_ms: 5000这一行,它将被编排层读取用于设置gRPC超时。
第二步:实现Python执行类(sms_sender_skill.py)
import json import logging from typing import Dict, Any, Optional import requests from base_skill import BaseSkill, ValidationError, SkillTimeoutError, SkillExecutionError class SmsSenderSkill(BaseSkill): def __init__(self, config: Dict[str, Any]): super().__init__(config) self.session = requests.Session() self.session.timeout = (3.0, 5.0) # 从配置加载白名单模板 self._allowed_templates = set(config.get("allowed_templates", [])) def health_check(self) -> Dict[str, Any]: # 简单检查HTTP会话是否可用 try: resp = self.session.get(f"{self.config['api_base']}/health", timeout=2.0) return {"status": "ok", "latency_ms": resp.elapsed.total_seconds() * 1000} except Exception as e: return {"status": "unhealthy", "error": str(e)} def validate_input(self, input_data: Dict[str, Any]) -> None: # 业务规则校验:模板ID必须在白名单中 if input_data.get("template_id") and input_data["template_id"] not in self._allowed_templates: raise ValidationError("template_id not approved by compliance team") def execute(self, input_data: Dict[str, Any]) -> Dict[str, Any]: try: resp = self.session.post( f"{self.config['api_base']}/v1/send", json=input_data, headers={"Authorization": f"Bearer {self.config['api_key']}"}, timeout=(3.0, 5.0) # 显式覆盖session timeout ) resp.raise_for_status() return resp.json() except requests.exceptions.Timeout: raise SkillTimeoutError("Carrier API timeout after 5.0s") except requests.exceptions.RequestException as e: raise SkillExecutionError(f"Carrier API error: {e}") def on_failure(self, input_data: Dict[str, Any], error: Exception) -> Optional[Dict[str, Any]]: # 失败降级:发送站内信 if isinstance(error, (SkillTimeoutError, SkillExecutionError)): return self._send_in_app_notification(input_data) return None def _send_in_app_notification(self, input_data: Dict[str, Any]) -> Dict[str, Any]: # 站内信发送逻辑(简化版) return { "notification_id": f"ia-{int(time.time())}", "channel": "in_app", "status": "sent" } def cleanup(self) -> None: self.session.close() def get_metrics(self) -> Dict[str, Any]: # 返回计数器快照 return { "http_requests_total": 1247, "avg_latency_ms": 213.5 } if __name__ == "__main__": # CLI入口:解析配置并启动 import argparse import yaml parser = argparse.ArgumentParser() parser.add_argument("--config", required=True) args = parser.parse_args() with open(args.config) as f: config = yaml.safe_load(f) skill = SmsSenderSkill(config) # 启动gRPC服务器(由base_skill提供) skill.serve_grpc(port=9001)第三步:编写配置文件(/etc/skills/sms-sender.yaml)
api_base: "https://api.carrier.example.com" api_key: "sk_live_abc123def456" allowed_templates: - "welcome_v2" - "order_confirm_v1" - "payment_reminder_v3"第四步:注册到Consul
# 1. 上传契约 curl -X PUT "http://10.0.1.10:8500/v1/kv/skills/sms-sender/v1/contract" \ --data-urlencode @/opt/skills/sms-sender/contract.yaml # 2. 注册gRPC端点(skill进程启动后自动获取) curl -X PUT "http://10.0.1.10:8500/v1/kv/skills/sms-sender/v1/endpoint" \ --data "10.0.1.11:9001" # 3. 手动触发一次健康检查(systemd会自动续期) curl -X PUT "http://10.0.1.10:8500/v1/kv/skills/sms-sender/v1/health?dc=dc1" \ --data "alive" --data-urlencode "ttl=30s"第五步:验证注册状态
# 查询Consul确认所有键已存在 curl "http://10.0.1.10:8500/v1/kv/skills/sms-sender/v1/?recurse" | jq '.[] | .Key' # 应输出: # "skills/sms-sender/v1/contract" # "skills/sms-sender/v1/endpoint" # "skills/sms-sender/v1/health" # 查看编排层日志,确认已加载skill journalctl -u orchestrator -n 50 --no-pager | grep "sms-sender" # 应看到类似:INFO orchestrator::registry > Loaded skill: sms-sender v1此时,sms-sender已作为标准skills接入系统。LLM调用层只需向orchestrator的gRPC接口发送ExecuteRequest,指定skill_name="sms-sender"和input_json='{"to":"13800138000","content":"Hello"}',即可获得执行结果。
4.3 审计能力实战:追踪一次短信发送的全链路
真正的skills系统价值,在于它让“谁在什么时候调用了什么能力、传了什么参数、得到什么结果”变得可追溯。我们模拟一次真实审计场景:
场景:合规部门要求提供“2023-10-15 14:00至15:00期间,所有发送给138****8000号码的短信内容及审批人”。
步骤1:在orchestrator中启用审计日志
修改/etc/orchestrator/config.yaml,添加:
audit: enabled: true log_level: "debug" retention_days: 90重启服务:sudo systemctl restart orchestrator
步骤2:触发一次调用
通过orchestrator的gRPC测试工具发送请求:
# 使用grpcurl(需提前安装) grpcurl -plaintext -d '{ "skill_name": "sms-sender", "version": "v1", "input_json": "{\"to\":\"13800138000\",\"content\":\"Test from audit demo\"}" }' 10.0.1.12:9090 orchestrator.Orchestrator/Execute步骤3:查询审计日志
orchestrator将生成结构化日志,存于/var/log/orchestrator/audit.log:
{ "timestamp": "2023-10-15T14:22:33.124Z", "request_id": "req_abc123def456", "skill_name": "sms-sender", "version": "v1", "caller": "llm-agent-prod", "input": { "to": "13800138000", "content": "Test from audit demo" }, "output": { "message_id": "msg_789xyz", "status": "sent" }, "execution_time_ms": 423.7, "audit_metadata": { "approver_id": "user-555", "approval_timestamp": "2023-10-15T14:22:32.881Z", "compliance_rule": "SMS_CONTENT_POLICY_V3" } }步骤4:编写审计查询脚本
# audit_query.py import json from datetime import datetime, timedelta def search_audit_logs(phone_number: str, start_time: str, end_time: str): logs = [] with open("/var/log/orchestrator/audit.log") as f: for line in f: try: log = json.loads(line.strip()) # 时间过滤 ts = datetime.fromisoformat(log["timestamp"].replace('Z', '+00:00')) if start_time <= ts <= end_time: # 号码匹配(支持模糊) if phone_number in log["input"]["to"]: logs.append(log) except (json.JSONDecodeError, KeyError): continue return logs if __name__ == "__main__": start = datetime(2023, 10, 15, 14, 0, 0) end = datetime(2023, 10, 15, 15, 0, 0) results = search_audit_logs("13800138000", start, end) for r