尧图网络科技YAOTU DIGITAL 获取报价
获取报价
首页 / 资讯中心 / 文章详情

AI-Infra-Guard实践:Docker部署与技能扫描漏报复盘

发布时间:2026/9/29 19:46:01

资讯中心
01
ARTICLE

AI-Infra-Guard实践:Docker部署与技能扫描漏报复盘

AI-Infra-Guard实践:Docker部署与技能扫描漏报复盘
上个月我们终于把 AI-Infra-Guard 从开发机搬到了生产环境用 Docker 一键拉起来那一瞬间我原本以为最难的部分已经过去了。结果第二天一早技能扫描的定时任务刚跑完群里就有人追问新上的 embedding 服务到底扫出来没有我看了一眼报告服务健康状态是 UP但技能列表里空空如也——该出现的技能没出现这就是一次典型的“漏报”。这篇就围绕这套工具的部署和一次技能扫描漏报的完整复盘来写分享给正在做 AI 服务统一纳管、想在基础设施层面把“能跑”升级为“能力可查”的团队参考。1. 为什么团队需要 AI-Infra-Guard从“服务能跑”到“能力可查”先说背景。我们团队维护的 AI 服务越来越多有文本生成的推理网关、embedding 服务、rerank 服务、语音合成、Agent 编排框架还有几个自研的模型服务。加起来四十多个部署单元每个都跑在 Kubernetes 里Prometheus 那一套监控也很完善CPU、内存、延迟、错误率都有看板。但真到产品要上线新功能时我经常被问到一个答不上来的问题现在到底哪些服务支持函数调用哪些 embedding 模型是当前可用的有没有支持 vision 的入口这些问题传统监控给不了答案。因为传统监控回答的是“服务活着吗、快不快”而产品需要知道的是“这个服务能干什么”。功能上线前我们只能翻文档、问同事或者自己拿 curl 一个个去试。文档这种东西只要维护跟不上很快就变成和线上真实状态脱节的废纸。后来我干脆写了个脚本挨个请求服务列表接口再把模型 ID 和已知能力做匹配。脚本日复一日地跑最终长成了 AI-Infra-Guard 这个工具。1.1 传统探活和技能扫描的本质差异举一个具体例子。一个模型服务 A 和一个 Agent 编排服务 B它们的 /health 都返回 200。传统监控只会记录“两个服务都健康”然后结束。但技能扫描会继续追问服务 A 的 /v1/models 里暴露了哪几个模型 ID这些 ID 对应的是文本生成还是 embedding服务 B 除了聊天补全是否支持 function calling 和 JSON mode这些能力在近期版本升级中是否有新增或下架这两层信息的价值完全不同。健康状态是瞬时的、二元的适合告警技能清单是相对稳定的、结构化的适合做能力规划和技术决策。AI-Infra-Guard 的设计核心就是把这两件事拆开底层用常规探活和依赖检查保证“守卫”能力上层用技能扫描定期刷新每个服务的技能画像并把结果落库、展示、对比历史变化。下面是它最初定下的四件套组件组件职责部署方式guard-api对外 API、扫描任务管理、技能画像读写Docker 容器暴露 8080guard-worker消费扫描任务执行健康检查和技能扫描插件Docker 容器内部运行 scannerguard-web管理面板展示扫描结果和技能差异Docker 容器暴露 8081PostgreSQL Redis元数据存储、技能快照、任务队列与缓存Docker 容器我选择用 Docker 作为第一版交付形态而不是直接上 Helm Chart原因是团队里既有云原生环境也有一些人仍然在本地虚拟机里试跑。一套 docker-compose 能让两种场景都快速起来后面确实也省了很多“为什么我环境起不来”的沟通成本。1.2 使用 AI-Infra-Guard 能解决什么问题落地之后它的核心收益有三点。第一服务目录从“人肉维护的表格”变成了“扫描出来的事实”。每个服务的模型列表、技能标签、最后扫描时间都是自动生成的产品同学自己就能在面板里查不用再来问研发。第二技能变化有迹可循。某个服务昨天还支持 rerank 技能今天扫描完消失了系统会高亮差异。第三接入新服务时有一个标准动作在配置文件里加一行目标地址跑一遍 dry-run确认技能识别结果符合预期再正式纳入周期扫描。当然它也远不是万能的后面要复盘的那次漏报就是典型的“扫描器以为自己跑得很正常、但报告里缺了关键内容”的坑。这个坑值得展开讲因为它不是配置写错而是工具设计上的盲区。2. Docker 一键起compose 编排与先踩的三个坑AI-Infra-Guard 的部署我一开始就想得很简单一个 docker-compose.yml 把依赖和应用全部拉起来开发环境一条命令搞定。实际写的时候才发现编排本身并不难难在让服务之间“有顺序地健康起来”以及让定时扫描任务的运行环境符合预期。先贴出当前生产正在用的精简版配置去掉了告警敏感信息保留核心结构version: 3.8 services: postgres: image: postgres:16-alpine environment: POSTGRES_DB: infra_guard POSTGRES_USER: guard POSTGRES_PASSWORD: guard_pass volumes: - pg_data:/var/lib/postgresql/data healthcheck: test: [CMD-SHELL, pg_isready -U guard -d infra_guard] interval: 5s timeout: 3s retries: 10 redis: image: redis:7-alpine command: redis-server --appendonly yes volumes: - redis_data:/data healthcheck: test: [CMD, redis-cli, ping] interval: 5s timeout: 3s retries: 10 guard-api: image: registry.example.com/ai-infra-guard/api:0.8.2 ports: - 8080:8080 environment: DATABASE_URL: postgresql://guard:guard_passpostgres:5432/infra_guard REDIS_URL: redis://redis:6379/0 TZ: Asia/Shanghai depends_on: postgres: condition: service_healthy redis: condition: service_healthy guard-worker: image: registry.example.com/ai-infra-guard/worker:0.8.2 environment: DATABASE_URL: postgresql://guard:guard_passpostgres:5432/infra_guard REDIS_URL: redis://redis:6379/0 SCAN_CONFIG_PATH: /config/services.yml TZ: Asia/Shanghai volumes: - ./config:/config:ro depends_on: guard-api: condition: service_healthy postgres: condition: service_healthy redis: condition: service_healthy guard-web: image: registry.example.com/ai-infra-guard/web:0.8.2 ports: - 8081:80 environment: API_BASE_URL: http://guard-api:8080 depends_on: - guard-api volumes: pg_data: redis_data:部署命令就三句docker compose pull拉镜像、docker compose up -d启动、docker compose logs -f worker观察扫描任务日志。外人看着确实“一键起”但我在第一个礼拜踩了三个和 Docker 环境强相关的坑写出来希望你能绕开。2.1 坑一容器健康检查只查了进程没有查业务就绪PostgreSQL 的 healthcheck 用的是pg_isreadyRedis 用redis-cli ping这两个都算是标准做法。但 guard-api 的 depends_on 如果只写postgres: service_healthy并不代表 API 已经能处理请求。因为 Java 系或 Python 系应用启动后仍要初始化连接池、加载路由这个阶段端口虽然是开的但请求会失败。我在本地复现过好几次worker 容器先起来了但它启动后立刻去注册扫描任务结果 guard-api 还没就绪任务注册失败worker 又不断重试产生了大量无效日志。解决方法是给 API 加一个应用层的健康检查接口并在 compose 里改成healthcheck: test: [CMD, curl, -f, http://localhost:8080/ready] interval: 5s timeout: 3s retries: 20/ready必须是业务代码里明确检查过数据库连接后再返回 200 的接口而不是简单的/health。我用这个替换之后启动顺序基本就不会乱了。2.2 坑二定时扫描任务碰上了容器时区问题worker 里跑着定时扫描调度默认会跟随容器的 UTC 时区。本地测试时我没注意部署到服务器后第二天看报告发现扫描记录上的时间戳比实际执行时间整整少了 8 小时。一开始我还以为是展示面板的 bug翻日志才发现是容器内TZ环境变量没设置cron 表达式按 UTC 理解每天凌晨两点触发的任务实际在早上十点跑看起来就像“没按时执行”。修复非常简单在 guard-api 和 guard-worker 两个容器里都加上TZ: Asia/Shanghai。如果你用的是自己的基础镜像一定记得一并检查镜像里的/etc/localtime配置。这个问题隐蔽在于监控工具自己的调度时间错了表面上还会显示“扫描成功”实际上执行周期已经面目全非。工具越“自动化”时间基准就越要显式规范化。2.3 坑三只读挂载和运行用户权限的错配./config:/config:ro这个只读挂载本来是为了安全防止容器里改了扫描配置。但 worker 启动时除了读配置文件还会尝试把它自己的工作目录下的tmp、日志目录初始化一遍。如果镜像里声明的运行用户 UID 和宿主机当前用户不一致Docker 的命名卷可能没问题宿主机目录 bind mount 就经常会出现 permission denied。我的建议是配置文件目录统一挂只读没问题但日志和临时文件不要往宿主机目录挂直接让容器用匿名卷或者显式给目录挂载授权。另外镜像内的运行用户固定写成uid10001宿主机对应目录用chown -R 10001:10001 ./config处理一次启动就不会再报权限的莫名其妙的错。3. 技能扫描到底在扫什么规则、匹配与判定逻辑把服务拉起来只是第一步真正有技术含量的是技能扫描模块。先给一个清晰的定义在 AI-Infra-Guard 里“技能”不是泛指能力而是可验证、可匹配的一组结构化信息包括模型 ID、推理类型、协议接口、辅助能力。一个技能要进入报告至少要满足“有证据、有来源、有置信度”这三个条件。具体扫描时它不会像压力测试那样对业务服务发起大量请求而是按照配置文件中声明的目标依次执行下面四个阶段基础探活访问目标的/health或配置的 health_path确认服务在线顺便拿到版本号、启动时间等元数据。元数据获取访问目标的/v1/models或配置的 skills_probe 路径拿到模型 ID 列表和服务描述信息。技能匹配把模型 ID、服务名、描述文本拿去做规则匹配在技能字典里找到对应技能。可选验证对匹配结果做一次轻量推理请求比如发一个几十 token 的调用确认该接口真的可用。3.1 一份可控的扫描配置长什么样扫描目标通过 services.yml 管理下面是我们在生产上使用的简化版本scan: interval: 0 */6 * * * default_auth_env: SERVICE_TOKEN targets: - name: llm-gateway base_url: http://llm-gateway.default.svc.cluster.local:8000 health_path: /health skills_probe: /v1/models auth_token_env: LLM_GATEWAY_TOKEN rule_groups: - model - protocol - name: embedding-v4-svc base_url: http://embedding-v4.default.svc.cluster.local:8000 health_path: /health skills_probe: /v1/models auth_token_env: EMBEDDING_SERVICE_TOKEN rule_groups: - model - inference这份配置里有两个容易被忽略的细节。第一interval是全局默认间隔但单个 target 可以在后面加自己的cron字段覆盖默认值。我建议不同目标错开扫描时间尤其是共享同一个网关的服务不要所有目标同一分钟触发否则瞬间大量探活请求可能会把网关打冒烟。第二skills_probe不一定是/v1/models。有的网关实现了/v1/models但返回的是网关自身信息真正的模型列表在管理端口这种就建议把该 target 的 probe 路径指到正确的管理接口。3.2 规则引擎与技能字典的匹配逻辑技能字典是一个 JSON 文件它定义了“看到什么特征就判定成什么技能”。例如{ skills: { text_generation: { aliases: [chat, instruct, gpt, qwen, llama], protocol_probe: { method: post, path: /v1/chat/completions, body: { model: {{model_id}}, messages: [{role: user, content: ping}], max_tokens: 1 }, success_when: choices[0].message.content ! null } }, embeddings: { aliases: [embed, embedding, text-embedding, bge, m3e, e5], protocol_probe: { method: post, path: /v1/embeddings, body: { model: {{model_id}}, input: ping }, success_when: data[0].embedding ! null } } }, precedence: [exact_id, alias_match, service_name, description_regex], unknown_tracking: true }匹配时按优先级来这是我认为整个规则引擎中最关键的一点。优先级最高的是“模型 ID 精确匹配”比如/v1/models返回的 ID 直接命中技能字典里某个条目那不需要任何额外探测置信度直接拉满。其次是别名匹配模型 ID 包含embedding出现且服务名也类似可以给出中高置信度的 embeddings 技能。再然后是服务描述文本的正则匹配。最后才是主动探测验证。回到标题里“漏报”的问题这次的漏报恰恰发生在别名匹配环节。我后面会专门复盘。这里先说明一个设计原则技能扫描的规则不能设计成“没有匹配就是没有技能”而应该是“没有匹配就进入未知技能跟踪”把识别不出来的模型 ID 作为一条独立记录存下来而不是悄悄丢掉。这个开关就是配置里的unknown_tracking: true。3.3 为什么主动探测必须设置熔断主动探测虽然能提高置信度但它是有副作用的你等于在向生产服务发起真实推理请求。如果配置里对每个模型都做一次 chat completion 或 embedding 调用扫描一遍下来成本不低而且可能触发服务侧的限流。我实际的经验是主动探测只用于“匹配置信度低于 0.7 但技能重要性高”的场景。比如某个模型 ID 是全新的字典里没有别名命中但健康接口返回的描述里包含 “text-embedding” 字样这时候就可以发一次 embedding 探测来确认。反过来如果精确匹配已经命中再探测就是纯浪费。同时给扫描器加上并发控制和超时单个探测请求超时 5 秒每目标并发不超过 2。这样即便配置失误也不会把业务请求链路拖垮。4. 漏报复盘一条技能是怎么从扫描报告里溜走的这次漏报的具体情况是这样的团队上线了一个新的 embedding 服务模型名是text-embedding-v4接口完全兼容 OpenAI 的/v1/embeddings。服务注册好之后我把它加进了扫描目标第二天看技能扫描报告发现健康状态是 UP但技能列表和前一天完全一样没有任何 embedding 技能的影子。群里的人关心的是“这个服务是不是没接好”而我最紧张的是为什么扫描器对新增服务一点反应都没有正常的排查链路应该是这样的。我先看扫描周期日志发现 worker 确实在凌晨按计划跑了一圈没有报错。接着去扫描结果表里查这个目标的快照结果发现skill_snapshot是空的但probe_status是 200。也就是说扫描器请求/v1/models是成功的拿到了模型 ID然后……后面就没有然后了。我又手动对着接口调了一遍/v1/models返回的内容里明明有text-embedding-v4。这时候问题就清楚了去看技能字典里embeddings的别名配置里面只有embed, embedding, text-embedding, bge, m3e, e5。把text-embedding-v4拿去做别名匹配时规则是按照“模型 ID 中包含这些别名关键词之一”来设计的text-embedding确实命中前缀。那为什么没匹配成功因为当时字典里还有一条硬编码的正则^text-embedding-(.*)-(large|small|base|tiny)$。我写这条正则的时候想的是过滤掉那些像 embeddings 但不是正式模型命名规则的版本结果它把text-embedding-v4这种“末尾直接跟版本号”的命名给排除了。4.1 根因一规则字典的更新速度落后于模型演进这是最直接的根因。模型服务的命名几乎没有规律可循今天叫text-embedding-v4明天就可能叫embed-v4-202405后天又可能是bge-large-zh-v1.5。如果技能字典全靠人工维护任何一次模型上新都意味着字典要跟着改。而人总会忘。漏报的那天晚上上线同事只在发布群里说了一句“新 embedding 服务好了”并没有人同步到扫描规则。规则里没有扫描器自然“视而不见”。4.2 根因二过分严格的正则把“该收录的”挡在门外我设计^text-embedding-(.*)-(large|small|base|tiny)$的本意是为了识别那些后缀是规格名的模型。但我忽略了一点模型版本的命名完全可以不用规格关键词结尾。当规则只认一种模式时它就变成了又一个“必须恰好匹配”的过滤器而不是“尽量识别”的分类器。技能扫描的价值在于把不确定的东西标记出来而不是把不确定的东西全部拦下。当时我太追求报告的干净宁可漏掉也不误报结果把一次正常的服务上线变成了静默漏报。4.3 根因三扫描器对“识别失败”采用了静默处理最值得反思的是这个设计盲区。扫描器拿到模型 ID 后如果字典里没匹配到任何技能旧版本的代码逻辑是把skill_snapshot写成空数组然后当作一次正常的扫描结果落库。从监控看扫描成功了、服务健康了、链路通了但报告里没有任何异常。这就是“静默漏报”的本质工具自己无法区分“目标确实没有技能”和“目标有技能但我没认出来”。从用户视角来看扫描结果和上次一样没人会注意到这个新服务没有技能记录。一旦有人依赖这个报告做能力规划就可能做出错误的判断。这个坑比“误报”更隐蔽因为误报至少会炸出声音漏报往往是安静地制造错误信息。4.4 修复动作把“未知技能”变成一等公民修复分成三步。第一步把text-embedding-v4加入技能字典并且把那条硬编码正则放宽为^(text-)?embed(ding)?[-:_]?v?[0-9.]*$同时保留对已知规格名large/small/base/tiny的单独映射。第二步打开全局开关unknown_tracking: true让所有/v1/models返回但未匹配到技能的模型 ID 进入unclassified_models表并在扫描报告里生成一条 warning。第三步对这个目标开启一次主动验证用模型 ID 去调用/v1/embeddings如果接口返回正常的向量数据就把技能embeddings的置信度标记为 0.95即使字典匹配逻辑将来又滞后了也能通过协议探测兜底。这三步做完重新手动触发了一次扫描。这次任务日志里出现了skill text-embedding-v4 - embeddings, confidence0.95的记录报告里也终于列出了新技能。整个排查过程前后花了两三个小时真正改代码和配置的时间不超过半小时剩下的时间全耗在“为什么一个看似成功的扫描结果会缺数据”的反复确认上。5. 从一次漏报带出的改进基线、dry-run 与告警闭环复盘完同事问了我一句这个坑是修一次就好还是能形成机制我说后者。漏报这类问题靠人工盯报告根本不现实团队的精力应该放在监听差异上。随后我给 AI-Infra-Guard 补充了三个机制现在看都是必要的。首先是基线快照与差异对比。每次扫描任务执行完系统会把整个技能清单写入skill_snapshots表同时带上本次扫描的 session_id。下次扫描开始前worker 会先读取该目标最近一次快照作为基线扫描结束后做一次 diff。新增技能、消失技能、置信度变化都要在结果页单独用状态标记展示。没有 diff 的扫描也必须在界面上留下记录避免“看起来是旧的但其实是刚跑完”的错觉。这次漏报之所以难发现就是因为新服务没有历史基线系统在第一次扫描时就该对“空快照”产生告警。其次是 dry-run 机制与新服务接入流程。现在在 services.yml 里新增扫描目标后我们会先跑一次guard scan --target new-service --dry-run。dry-run 会完整执行探活、元数据获取、技能匹配和主动探测唯一区别是不写库、不触发告警。接入清单里明确要求 dry-run 的输出必须检查两项探测路径的 HTTP 状态是否为 200以及unclassified_models里有没有意外内容。如果模型 ID 在 dry-run 阶段就进了未分类列表接入人员就必须修复规则或者补充字典否则不允许纳入周期扫描。用这个动作把漏报风险尽量前置。最后是扫描器自身的质量告警。我们把以下三种情况视为扫描器自身的“异常事件”而不是业务服务的故障第一某个目标连续多次扫描技能快照始终为空且探活成功第二unclassified_models数量突然超过历史均值三倍以上第三主动探测成功率异常下降。这三种情况都会触发 webhook 告警把事件推到值班群。附一个最小化的告警配置alerting: webhook_url: https://alert.example.com/hooks/ai-infra-guard triggers: - empty_snapshot_with_healthy_target - unclassified_models_surge - probe_verified_failure_rate_gt: 0.3 deduplicate_minutes: 30这里我想强调一点告警的意义不在于把每一次漏报都实时抓到而在于让“扫描器自己不确定”的状态可见。漏报发生的第一现场是“有模型但没匹配上”只要这个状态被记录下来并通知到人后续的规则补充只是时间问题。真正可怕的是系统连“它没看懂”都不告诉你。再补充几条使用经验。技能字典的变更我建议走代码评审流程不允许有人在生产环境直接改规则。字典改完之后至少要跑一次针对所有目标的 dry-run确认没有把旧技能误伤。规则优先级也要小心先把精确 ID 放最前面再放别名最后才是描述正则优先级错乱会导致同名模型被误判成别的技能。扫描周期不是越短越好对生产推理服务来说每六个小时一次已经足够太频繁反而容易给网关制造无意义负载。还有一点关于“漏报”的心态。那次复盘之后我对监控工具自己的输出也保持警惕了。过去我的第一反应是“报告说没问题就没问题”现在我会问一句报告里的“没有问题”是基于完整数据还是只是没有匹配到异常尤其对于 AI 基础设施这种变化快的领域服务能力的增加、模型版本的上线都比传统 Web 服务的变更频繁得多。把“未识别”当作一个状态去对待比拼命提高规则覆盖率更有效。测试技能扫描系统时也别只测它能不能发现故意制造的故障更值得做的测试是塞一个全新的、字典里不存在但实际可用的技能看它会不会给出未知告警。如果它把未知静默吞掉了那这个系统的报告可信度就要打个问号。最后再分享一个我后来养成的习惯。每次有新模型上线我不再看别人提供的模型清单而是直接去 AI-Infra-Guard 的技能扫描报告里搜模型 ID。如果当天报告里没出现要么服务还没真正暴露接口要么扫描规则又没跟上。这套“以扫描结果为准”的工作流让我们从被动等人的通知变成了主动核实线上状态。一次漏报带来的最大收获不是修了一行正则而是把整个团队的信任基准从“接口文档说了算”改成了“扫描证据说了算”。
02
RELATED NEWS

相关资讯

更多网站建设与数字化升级内容

03
WHY YAOTU

想打造同款高转化官网?

懂行业、懂生意,从建站到增长一站式陪跑

◈

场景化定制

不做模板站,围绕你的业务场景量身设计,小众不撞款。

◐

营销型架构

以转化目标组织内容与路径,让官网真正带来询盘。

▲

全周期服务

设计、开发、运营、运维一体,上线只是开始。

免费获取你的建站方案

留下需求,专属顾问 24 小时内为你输出方案建议。