Agent技能可验证中枢:基于HTTP契约的可靠能力治理 1. 项目概述这不是一个“网站”而是一套面向 Agent 的可验证能力中枢“为了让 AI 更可靠我做了个给 Agent 用的网站含 skills”——这句话乍看像极了某位开发者在朋友圈发的轻描淡写式总结但实际拆开来看它精准锚定了当前大模型应用落地中最棘手的三个断层可信断层、能力断层、验证断层。我过去三年深度参与过多个面向生产环境的 Agent 系统搭建从金融风控辅助到工业设备故障推理反复踩过的坑都指向同一个事实一个没有明确定义、不可独立验证、无法被外部调用的能力skill本质上不是能力而是黑盒幻觉的温床。所谓“给 Agent 用的网站”绝非传统意义上供人浏览的前端页面而是一个以 HTTP 接口为载体、以结构化协议为契约、以原子化技能为单元、具备身份认证与执行审计能力的服务中枢。它的核心关键词——“AI 更可靠”“Agent”“网站”“skills”——每一个都不是修饰词而是技术选型的硬约束条件。这个项目解决的不是“怎么让 Agent 更聪明”而是“怎么让 Agent 的每一次调用都可追溯、可复现、可归责”。比如当一个采购 Agent 调用“比价技能”时系统必须能明确回答调用的是哪个版本的比价逻辑输入参数是否符合预设 schema执行过程是否触发了风控阈值结果是否经过第三方校验这些信息不能藏在日志里也不能靠人工回溯而要像查快递物流一样输入一个 skill_id 就能拉出全链路凭证。因此这个“网站”的本质是Agent 能力的注册中心 执行网关 验证仪表盘三位一体。它不替代 Agent 的规划与决策模块但为所有决策提供可信赖的“肌肉”——那些被严格定义、沙箱隔离、带签名返回的 skills。适合正在构建企业级 Agent 应用的工程师、需要向业务方证明 AI 行为合规性的技术负责人以及对“LLMTool”范式有实操经验、正卡在“技能越来越多但谁也不敢信”的阶段的开发者。它不教你怎么写 prompt但会告诉你当你的 Agent 说“我已调用天气 API”你该信几分又该去哪查证。2. 整体设计思路为什么必须是“网站”形态而不是 SDK 或私有协议2.1 “网站”是唯一能同时满足四重信任需求的载体很多人第一反应是“技能为什么不直接封装成 Python 函数或 gRPC 服务”这恰恰是本项目设计最核心的破局点。我们来拆解 Agent 在真实场景中面临的四重信任压力再看为什么只有“网站”形态能一并扛住跨语言信任一个由 Python 构建的规划 Agent需要调用由 Rust 编写的库存查询 skill再触发 Java 写的合同生成 service。如果每个 skill 都要求 Agent 侧引入对应语言的 SDK版本管理、错误码映射、超时配置将变成噩梦。而 HTTP 是所有现代语言原生支持、语义清晰、调试工具链最成熟的协议。curl 一下就能验证Postman 里点几下就能重放这是任何私有协议无法比拟的“人类可读性”。运行时隔离信任技能执行可能涉及文件读写、数据库连接、外部 API 调用。若 skill 直接作为函数注入 Agent 进程一次内存泄漏或未捕获异常就可能导致整个 Agent 崩溃。而网站形态天然意味着进程隔离——每个 skill 运行在独立容器或进程内通过 HTTP 通信。我们实测过当一个 PDF 解析 skill 因恶意文件触发无限循环时Agent 主进程 CPU 占用率纹丝不动只是该 skill 的 HTTP 接口返回 503。审计与合规信任金融、医疗等强监管行业要求所有 AI 辅助决策留痕。HTTP 请求/响应天然携带时间戳、源 IP可映射到 Agent 实例、完整 payload 和 signature。我们设计的网站强制所有 skill 调用必须携带X-Agent-ID和X-Request-Signature头后端自动记录到审计表。某次客户审计时仅用 15 分钟就导出了某次贷款审批中所有 skill 调用的原始请求、返回、耗时、签名验证结果而用 SDK 方案则需额外开发日志解析器。能力发现与治理信任当团队有 50 个 skill 时如何让新加入的 Agent 开发者快速知道“有哪些能力可用参数怎么填谁在维护SLA 是多少”一个集中式的、带搜索和文档渲染的网站页面比散落在 Git 仓库里的 README.md 文件有效十倍。我们内置了 OpenAPI 3.0 自动生成所有 skill 的接口定义、示例、错误码实时同步到网站文档页连 curl 示例都一键可复制。提示这里说的“网站”不是指必须有炫酷 UI。最小可行版本可以只有/healthz、/skills/{id}/invoke和/docs三个路由但它的存在本身就构建了一层标准化的信任基础设施。2.2 Skills 不是 API而是“契约化能力单元”很多项目把 skill 等同于“调用一个 API”这是危险的简化。本项目定义的 skill 是一个包含元数据、执行逻辑、验证规则、生命周期钩子的完整契约单元。一个 skill 的完整定义包含字段类型必填说明实际案例skill_idstring是全局唯一标识遵循domain:verb:noun:version格式finance:calculate:apr:v2.1namestring是人类可读名称“年化利率计算器”descriptionstring是功能、适用场景、限制说明“仅适用于固定利率贷款不支持浮动利率...”input_schemaJSON Schema是严格定义输入参数结构与校验规则强制loan_amount 0,term_months∈ [1,360]output_schemaJSON Schema是定义期望输出结构用于结果可信度校验apr_rate必须是 0.0 到 1.0 的 numberexecution_timeout_msinteger是最大允许执行时间超时强制终止3000msrequired_permissionsarray否所需权限列表用于 RBAC 控制[read:loan_data, write:audit_log]verification_hooksobject否执行后自动触发的验证逻辑调用credit_score_service校验 APR 是否在合理区间这个定义被固化在每个 skill 的skill.yaml文件中并在部署时由网站后端加载、校验、注册。这意味着当 Agent 发起调用时网站首先做的不是执行代码而是校验请求是否符合input_schema、调用者是否有required_permissions、目标 skill 是否处于active状态。这种前置契约检查把大量潜在错误拦截在执行之前是可靠性最基础的防线。2.3 架构分层为什么放弃“大一统”框架选择“薄网关厚技能”我们刻意避开了主流 Agent 框架如 LangChain 的 Tool、LlamaIndex 的 QueryEngine提供的“一站式” skill 管理方案原因很现实它们把 skill 的定义、执行、验证、监控耦合在同一个运行时里导致升级、调试、审计成本指数级上升。我们的架构采用清晰的三层分离Agent 层无状态只负责规划、记忆、调用决策。它通过标准 HTTP Client如 Python 的httpx向网站发起POST /skills/{id}/invoke请求传入 JSON body。它不关心 skill 怎么实现只认skill_id和返回的 JSON 结构。网关层极薄由 Go 编写的高性能 HTTP 服务构成核心逻辑只有 300 行代码。它只做四件事1) 解析 JWT Token 验证 Agent 身份2) 根据skill_id查找注册信息3) 用input_schema校验请求体4) 将请求转发给对应 skill 的执行端点。它不做任何业务逻辑不碰数据库不存状态因此可以水平无限扩展。Skill 层极厚每个 skill 是一个独立部署的微服务Docker 容器拥有自己的数据库连接池、缓存、重试策略、熔断器。例如“邮件发送 skill” 自带 SMTP 连接池和失败队列“图像识别 skill” 自带 GPU 资源管理和模型热加载。它们通过/healthz和/invoke两个标准端点暴露给网关完全自治。这种分层带来的最大好处是故障域隔离。网关层宕机所有 skill 仍可被直连测试某个 skill 崩溃只影响该能力Agent 可降级或重试。我们在某次压测中故意 kill 掉“PDF 解析 skill”的容器Agent 在 2 秒内收到503 Service Unavailable并根据预设策略切换到备用 OCR 服务全程无感知。而如果所有 skill 都跑在 LangChain 的同一个 Python 进程里一次 OOM 就会让整个 Agent 归零。3. 核心细节与实操要点从定义到上线的完整闭环3.1 Skill 定义YAML 是契约不是配置一个 skill 的生命始于skill.yaml文件。这不是简单的配置文件而是一份具有法律效力的技术契约当然这里的“法律效力”指团队内部的协作约定。我们强制所有 skill 必须包含以下部分缺失任一项则拒绝注册# finance:calculate:apr:v2.1 skill_id: finance:calculate:apr:v2.1 name: 年化利率计算器 description: | 根据贷款本金、期限、月还款额计算年化利率APR。 注意仅适用于等额本息还款方式不支持提前还款计算。 输入金额单位为人民币分整数避免浮点精度问题。 input_schema: type: object required: [loan_amount_cents, term_months, monthly_payment_cents] properties: loan_amount_cents: type: integer minimum: 10000 # 最低100元 description: 贷款本金单位分 term_months: type: integer minimum: 1 maximum: 360 description: 贷款期限单位月 monthly_payment_cents: type: integer minimum: 1 description: 每月还款额单位分 output_schema: type: object required: [apr_rate, calculation_method] properties: apr_rate: type: number minimum: 0.0 maximum: 1.0 description: 年化利率范围0.0-1.0即0%-100% calculation_method: type: string enum: [Newton-Raphson, Bisection] description: 使用的数值求解算法 execution_timeout_ms: 2500 required_permissions: [read:loan_data] # 验证钩子确保计算结果在合理业务区间内 verification_hooks: - name: apr_range_check type: jsonpath expression: $.apr_rate 0.05 $.apr_rate 0.36 error_message: APR 计算结果超出合理范围5%-36%请检查输入参数这个 YAML 的关键在于所有字段都服务于“可验证性”description中的“单位为人民币分”直接规避了浮点数精度陷阱这是我们在某次线上事故后加的——当输入loan_amount_cents: 100000010万元时如果 skill 代码里错误地用了float除法1000000 / 100.0可能产生9999.999999999998导致后续计算偏差。input_schema的minimum/maximum不是装饰网关层会用 jsonschema 库进行严格校验非法请求在抵达 skill 之前就被400 Bad Request拦截。verification_hooks的jsonpath表达式会在 skill 返回后立即执行如果apr_rate是0.0010.1%钩子会触发并返回自定义错误而非让 Agent 误以为这是一个有效结果。实操心得我们曾要求一个新加入的 junior 工程师为“汇率查询 skill”写skill.yaml。他第一版写了type: string的exchange_rate被立刻打回。正确的做法是type: number并加上multipleOf: 0.0001因为汇率通常精确到小数点后4位。这个细节强迫团队所有人建立“数据契约思维”而不是“能跑就行”的心态。3.2 网关实现Go 的简洁性是可靠性的基石网关层我们选用 Go核心考量是极致的启动速度、确定性的 GC 行为、无依赖的二进制分发、以及对高并发 HTTP 的原生友好。一个精简的网关核心逻辑如下伪代码实际代码约 280 行// 1. 初始化加载所有 skill.yaml 到内存 map[skill_id]SkillMeta skills : loadSkillsFromDir(/etc/skills) // 2. HTTP 路由只处理 /skills/{id}/invoke http.HandleFunc(/skills/, func(w http.ResponseWriter, r *http.Request) { if r.Method ! POST || !strings.HasPrefix(r.URL.Path, /skills/) { http.Error(w, Method Not Allowed, http.StatusMethodNotAllowed) return } // 解析 skill_id: /skills/finance:calculate:apr:v2.1/invoke - finance:calculate:apr:v2.1 parts : strings.Split(strings.TrimPrefix(r.URL.Path, /skills/), /) if len(parts) 2 || parts[1] ! invoke { http.Error(w, Invalid path, http.StatusBadRequest) return } skillID : parts[0] // 3. 查找 skill 元数据 meta, ok : skills[skillID] if !ok { http.Error(w, Skill not found, http.StatusNotFound) return } // 4. JWT 认证从 Authorization header token, err : parseAndValidateJWT(r.Header.Get(Authorization)) if err ! nil { http.Error(w, Unauthorized, http.StatusUnauthorized) return } // 5. 权限检查token 中的 claims 是否包含 meta.required_permissions if !hasPermissions(token.Claims, meta.RequiredPermissions) { http.Error(w, Forbidden, http.StatusForbidden) return } // 6. 读取并校验请求体使用 jsonschema var reqBody map[string]interface{} if err : json.NewDecoder(r.Body).Decode(reqBody); err ! nil { http.Error(w, Invalid JSON, http.StatusBadRequest) return } if err : validateWithSchema(reqBody, meta.InputSchema); err ! nil { http.Error(w, Validation failed: err.Error(), http.StatusBadRequest) return } // 7. 转发请求到 skill 的 /invoke 端点带超时 skillURL : fmt.Sprintf(http://%s:%d/invoke, meta.Host, meta.Port) client : http.Client{ Timeout: time.Duration(meta.ExecutionTimeoutMs) * time.Millisecond, } skillResp, err : client.Post(skillURL, application/json, bytes.NewReader(reqBodyBytes)) // 8. 处理 skill 响应成功则返回失败则记录审计日志并透传错误 if err ! nil { auditLog(skillID, ERROR, network_failure, err.Error()) http.Error(w, Skill unavailable, http.StatusServiceUnavailable) return } defer skillResp.Body.Close() // 9. 读取 skill 返回的 JSON var skillResult map[string]interface{} if err : json.NewDecoder(skillResp.Body).Decode(skillResult); err ! nil { auditLog(skillID, ERROR, invalid_response, non-json response) http.Error(w, Invalid skill response, http.StatusInternalServerError) return } // 10. 执行 verification_hooks for _, hook : range meta.VerificationHooks { if !runVerificationHook(skillResult, hook) { auditLog(skillID, VERIFICATION_FAIL, hook.Name, hook.ErrorMessage) http.Error(w, hook.ErrorMessage, http.StatusInternalServerError) return } } // 11. 记录成功审计日志返回结果 auditLog(skillID, SUCCESS, ok, ) w.Header().Set(Content-Type, application/json) json.NewEncoder(w).Encode(skillResult) })这个实现的关键在于所有路径都是线性的、无分支的、无状态的。没有数据库连接没有缓存没有异步队列。它就是一个“智能路由器”职责单一到极致。这带来了两个直接好处一是性能极高单核 CPU 在 10K QPS 下 CPU 占用不到 30%二是故障排查极其简单——如果/skills/xxx/invoke慢要么是 JWT 解析慢检查密钥轮换要么是网络转发慢pingskill host要么是 skill 本身慢看 skill 日志不存在“网关内部逻辑卡住”的模糊地带。3.3 Skill 开发每个技能都是一个微型 SaaS我们要求每个 skill 必须是一个独立的、可单独部署、可单独测试、可单独监控的微型服务。以“邮件发送 skill”为例其目录结构强制为email-sender/ ├── skill.yaml # 技能契约定义 ├── main.go # 入口只实现 /healthz 和 /invoke 两个端点 ├── handler/ # 业务逻辑 │ ├── smtp_client.go # 封装 SMTP 连接池 │ └── template.go # 邮件模板渲染 ├── config/ # 配置管理env vars or config file ├── test/ # 包含完整的端到端测试 │ └── e2e_test.go # 模拟网关调用验证输入/输出/错误流 └── Dockerfile # 标准化构建main.go的核心逻辑只有两个函数func healthzHandler(w http.ResponseWriter, r *http.Request) { // 检查 SMTP 连接池是否健康 if !smtpClient.IsHealthy() { http.Error(w, SMTP unhealthy, http.StatusServiceUnavailable) return } w.WriteHeader(http.StatusOK) w.Write([]byte(OK)) } func invokeHandler(w http.ResponseWriter, r *http.Request) { // 1. 解析请求体已由网关校验过 schema此处只需反序列化 var req SendEmailRequest if err : json.NewDecoder(r.Body).Decode(req); err ! nil { http.Error(w, Invalid request, http.StatusBadRequest) return } // 2. 执行业务逻辑渲染模板、发送邮件 result, err : sendEmail(req) if err ! nil { // 3. 将领域错误映射为标准 HTTP 状态码 switch err.(type) { case *ValidationError: http.Error(w, err.Error(), http.StatusBadRequest) case *RateLimitError: http.Error(w, Too many requests, http.StatusTooManyRequests) default: http.Error(w, Internal error, http.StatusInternalServerError) } return } // 4. 返回结构化结果符合 output_schema w.Header().Set(Content-Type, application/json) json.NewEncoder(w).Encode(result) }这种设计让 skill 开发者可以完全聚焦于业务逻辑而无需关心 Agent 如何调用、如何重试、如何熔断。所有横切关注点认证、限流、监控都由网关或基础设施如 Kubernetes Ingress统一处理。我们甚至为每个 skill 提供了标准的 Prometheus metrics endpoint (/metrics)暴露skill_invoke_total,skill_invoke_duration_seconds等指标运维团队用一套 Grafana 看板就能监控全部 50 个 skill 的健康状况。4. 实操过程从零开始部署一个可验证的“天气查询”Skill4.1 步骤一定义 weather:query:current:v1.0创建weather-skill/skill.yamlskill_id: weather:query:current:v1.0 name: 实时天气查询 description: | 根据城市名称查询当前天气温度、湿度、风速、天气状况。 数据源公开气象 API需配置 API_KEY。 注意城市名必须为中文且为国内主要城市北京、上海、广州等。 input_schema: type: object required: [city_name] properties: city_name: type: string minLength: 2 maxLength: 10 pattern: ^[\u4e00-\u9fa5]$ # 纯中文 description: 城市中文名称 output_schema: type: object required: [temperature_c, humidity_percent, wind_speed_kph, condition] properties: temperature_c: type: number minimum: -50 maximum: 50 description: 当前温度摄氏度 humidity_percent: type: integer minimum: 0 maximum: 100 description: 相对湿度百分比 wind_speed_kph: type: number minimum: 0 description: 风速公里/小时 condition: type: string enum: [晴, 多云, 阴, 雨, 雪, 雾, 霾] description: 天气状况 execution_timeout_ms: 3000 required_permissions: [read:weather_api] verification_hooks: - name: temperature_reasonableness type: jsonpath expression: $.temperature_c -40 $.temperature_c 45 error_message: 温度值超出地球常见范围请检查数据源 - name: condition_valid type: jsonpath expression: [晴,多云,阴,雨,雪,雾,霾].includes($.condition) error_message: 天气状况不在预设枚举中注意pattern: ^[\u4e00-\u9fa5]$这一行是血泪教训。早期版本允许英文城市名结果有人传入New YorkAPI 返回 404但 skill 错误地将空响应解析为{temperature_c: 0}导致 Agent 以为纽约气温是 0°C。强制中文校验从源头杜绝了这类问题。4.2 步骤二开发 weather-skill 服务weather-skill/main.gopackage main import ( encoding/json fmt io log net/http os time ) type WeatherRequest struct { CityName string json:city_name } type WeatherResponse struct { TemperatureC float64 json:temperature_c HumidityPercent int json:humidity_percent WindSpeedKph float64 json:wind_speed_kph Condition string json:condition } var ( weatherAPIKey os.Getenv(WEATHER_API_KEY) weatherAPIURL https://api.example-weather.com/v1/current ) func healthzHandler(w http.ResponseWriter, r *http.Request) { // 简单检查 API KEY 是否存在 if weatherAPIKey { http.Error(w, API_KEY missing, http.StatusServiceUnavailable) return } w.WriteHeader(http.StatusOK) w.Write([]byte(OK)) } func invokeHandler(w http.ResponseWriter, r *http.Request) { var req WeatherRequest if err : json.NewDecoder(r.Body).Decode(req); err ! nil { http.Error(w, Invalid JSON, http.StatusBadRequest) return } // 调用外部天气 API带超时 client : http.Client{Timeout: 2500 * time.Millisecond} apiURL : fmt.Sprintf(%s?city%skey%s, weatherAPIURL, req.CityName, weatherAPIKey) resp, err : client.Get(apiURL) if err ! nil { log.Printf(Weather API call failed: %v, err) http.Error(w, Weather service unavailable, http.StatusServiceUnavailable) return } defer resp.Body.Close() if resp.StatusCode ! http.StatusOK { log.Printf(Weather API returned %d, resp.StatusCode) http.Error(w, Weather API error, http.StatusBadGateway) return } // 解析 API 响应 var apiResp struct { Data struct { TempC float64 json:temp_c Humidity int json:humidity WindKph float64 json:wind_kph Condition string json:condition_text } json:current } if err : json.NewDecoder(resp.Body).Decode(apiResp); err ! nil { log.Printf(Failed to decode weather API response: %v, err) http.Error(w, Invalid weather data, http.StatusInternalServerError) return } // 映射到 skill 的 output_schema result : WeatherResponse{ TemperatureC: apiResp.Data.TempC, HumidityPercent: apiResp.Data.Humidity, WindSpeedKph: apiResp.Data.WindKph, Condition: mapCondition(apiResp.Data.Condition), } w.Header().Set(Content-Type, application/json) json.NewEncoder(w).Encode(result) } // 将 API 返回的 condition_text 映射为预设枚举 func mapCondition(cond string) string { mapping : map[string]string{ Sunny: 晴, Partly cloudy: 多云, Overcast: 阴, Rain: 雨, Snow: 雪, Fog: 雾, Haze: 霾, } if v, ok : mapping[cond]; ok { return v } return 阴 // 默认 fallback } func main() { http.HandleFunc(/healthz, healthzHandler) http.HandleFunc(/invoke, invokeHandler) port : os.Getenv(PORT) if port { port 8080 } log.Printf(Weather skill listening on port %s, port) log.Fatal(http.ListenAndServe(:port, nil)) }DockerfileFROM golang:1.21-alpine AS builder WORKDIR /app COPY go.mod go.sum ./ RUN go mod download COPY . . RUN CGO_ENABLED0 GOOSlinux go build -a -installsuffix cgo -o weather-skill . FROM alpine:latest RUN apk --no-cache add ca-certificates WORKDIR /root/ COPY --frombuilder /app/weather-skill . EXPOSE 8080 CMD [./weather-skill]4.3 步骤三部署与注册构建并推送镜像cd weather-skill docker build -t your-registry.com/weather-skill:v1.0 . docker push your-registry.com/weather-skill:v1.0在 Kubernetes 中部署假设使用 K8s# weather-skill-deployment.yaml apiVersion: apps/v1 kind: Deployment metadata: name: weather-skill-v1 spec: replicas: 2 selector: matchLabels: app: weather-skill-v1 template: metadata: labels: app: weather-skill-v1 spec: containers: - name: weather-skill image: your-registry.com/weather-skill:v1.0 ports: - containerPort: 8080 env: - name: WEATHER_API_KEY valueFrom: secretKeyRef: name: weather-api-secret key: api-key --- apiVersion: v1 kind: Service metadata: name: weather-skill-v1 spec: selector: app: weather-skill-v1 ports: - port: 8080 targetPort: 8080更新网关的 skills 配置将weather-skill/skill.yaml放入网关的/etc/skills/目录并重启网关或实现热加载。网关启动时会扫描该目录加载所有 YAML构建skillsmap。验证端到端流程# 1. 检查网关是否注册成功 curl http://gateway-host/skills/weather:query:current:v1.0 # 2. 模拟 Agent 调用注意实际 Agent 会带 JWT Token curl -X POST http://gateway-host/skills/weather:query:current:v1.0/invoke \ -H Content-Type: application/json \ -d {city_name: 北京} # 预期返回符合 output_schema # {temperature_c:25.3,humidity_percent:65,wind_speed_kph:12.5,condition:晴} # 3. 测试边界情况传入非法城市名 curl -X POST http://gateway-host/skills/weather:query:current:v1.0/invoke \ -H Content-Type: application/json \ -d {city_name: New York} # 预期返回 400 Bad Request因为 pattern 校验失败整个过程从写 YAML 到服务上线熟练开发者可在 30 分钟内完成。关键是所有环节都有明确的、可自动化的验证点YAML 格式校验、schema 有效性校验、Docker 构建测试、K8s 部署健康检查、网关注册日志、端到端 HTTP 调用测试。没有一步是“凭感觉”。5. 常见问题与排查技巧实录那些文档里不会写的坑5.1 问题速查表高频故障与定位路径现象可能原因定位命令/步骤解决方案调用 skill 返回404 Not Found1.skill_id拼写错误大小写、冒号、版本号2. 网关未加载该 skillYAML 文件未放入正确目录或格式错误3. 网关未重启/热加载失败curl http://gateway/skills查看已注册列表kubectl logs -l appgateway | grep loading skill严格按domain:verb:noun:version格式命名检查 YAML 语法用yamllint确认网关配置挂载路径调用 skill 返回400 Bad Request但提示信息模糊1.input_schema校验失败但错误信息未透传2. 请求体 JSON 格式错误如末尾逗号curl -v ...查看完整响应头和 body用jq格式化请求体echo {city_name: 北京} | jq .在网关日志中开启DEBUG级别查看jsonschema的具体校验错误使用在线 JSON Schema Validator 测试 schema调用 skill 返回503 Service Unavailable1. skill 服务未启动或崩溃2. skill 的/healthz返回非 2003. 网关到 skill 的网络不通DNS、防火墙kubectl get pods -l appweather-skill-v1kubectl logs -l appweather-skill-v1kubectl exec -it gateway-pod -- ping weather-skill-v1检查 skill Pod 状态和日志确认/healthz端点返回200 OK检查 K8s Service DNS 解析skill 返回结果正确但 Agent 报错“无法解析响应”1. skill 返回的 JSON 字段名与output_schema中定义的不一致大小写、下划线2.output_schema的type定义错误如temperature_c定义为string但 skill 返回numbercurl ... | jq keys查看实际返回字段用jsonschemaCLI 工具验证echo {temperature_c:25} | jsonschema -i - weather-skill/skill.yaml严格遵循output_schema的字段名和类型在 skill 的invokeHandler中用json.Marshal时确保字段名匹配Go 中用json:temperature_ctagAudit Log 中显示VERIFICATION_FAIL但 skill 返回正常1.verification_hooks的jsonpath表达式语法错误2. 表达式逻辑有误如$.temperature_c 50但实际值是45.5在网关日志中查找verification_fail关键字看具体error_message用在线 JSONPath 测试器验证表达式使用标准 JSONPath 语法避免$.data.temp这种非标准写法在verification_hooks中添加debug: true字段网关 DEBUG 日志会打印jsonpath执行的中间值5.2 独家避坑技巧来自生产环境的 3 个血泪经验**技巧一为input_schema的