OpenClaw智能体运行时:YAML驱动的AI技能操作系统
1. 项目概述OpenClaw不是“另一个LLM工具”而是面向真实工作流的智能体操作系统OpenClaw这个词最近在技术社区里出现频率陡增但很多人点开GitHub仓库第一眼就懵了——它既不像Dify那样有可视化编排界面也不像LangChain那样堆满文档示例更不提供开箱即用的聊天窗口。我第一次跑通它的hello_world技能时终端只输出了一行带时间戳的JSON{status:success,output:Hello from OpenClaw v0.8.3}。没有炫酷UI没有模型加载动画甚至没告诉你这行结果是怎么从Python函数变成API响应的。但正是这种“反直觉”的设计恰恰暴露了OpenClaw的真实定位它压根不是给终端用户用的而是给工程师、SRE、自动化运维团队和AI产品架构师准备的智能体运行时环境Agent Runtime。你可以在标题里看到“最新版”“部署”“技能”这些词但它们背后指向的是三个完全不同的技术层级版本迭代解决的是能力边界问题比如v0.8.3新增了对MinerU文档解析器的原生支持部署形态决定的是资源调度与安全隔离策略本地Docker vs Railway云托管而“技能”根本不是功能按钮它是可热重载、可版本化、可依赖管理的独立服务单元Skill as a Service。举个最典型的例子当你的团队需要把“自动从飞书多维表格拉取销售线索→调用DeepSeek-Coder生成客户画像摘要→通过企业微信机器人推送至销售群”这个流程固化下来OpenClaw的skill目录下就该出现三个文件fetch_leads.py、generate_profile.py、notify_sales.py每个文件都自带requirements.txt和skill.yaml元数据。这不是写脚本这是在定义服务契约。所以别被“Claw”爪这个字误导——它不强调抓取能力而是指代精准、可伸缩、可组合的执行末端。就像机械臂的末端执行器OpenClaw不关心你用什么大模型做推理只确保你的generate_profile.py能在GPU节点上稳定运行300次/分钟且每次失败都能触发预设的降级逻辑比如切到CPU版轻量模型。这也是为什么所有热词里反复出现railway部署和dify本地部署的对比Dify是面向AI应用开发者的低代码平台而OpenClaw是面向AI基础设施工程师的Kubernetes级抽象层。如果你正在为团队构建AI能力中台或者需要把多个大模型API、私有知识库、内部系统API编织成可审计、可灰度、可回滚的智能工作流那么OpenClaw不是“可选项”而是当前技术栈里少有的、真正把“智能体”当作一等公民来管理的开源方案。它不教你怎么写Prompt它教你如何让Prompt工程成果变成生产环境里可交付、可计费、可监控的服务单元。2. 核心设计哲学为什么OpenClaw放弃图形界面选择YAMLPython双轨制OpenClaw的架构决策几乎每一条都在挑战主流AI工具链的设计惯性。最刺眼的莫过于它彻底放弃Web UI所有配置、技能注册、环境变量注入全部通过YAML文件完成。很多人第一反应是“这太反人类了”直到他们被迫在Dify里为同一个技能配置5次不同环境的API Key、在LangChain里为每个Chain手动注入相同的LLM参数、在AutoGen里为每个Agent重复写llm_config——这时才明白OpenClaw的YAML不是妥协而是对配置漂移Configuration Drift的主动防御。2.1 技能定义的三层契约YAML元数据是服务治理的起点一个OpenClaw技能绝不是简单扔个Python文件进去。以官方示例中的weather_skill.py为例它必须配套一个同名的weather_skill.yamlname: weather-forecast version: 1.2.0 description: Fetch real-time weather data for specified city author: ops-teamcompany.com tags: [public-api, cache-enabled] timeout: 30 max_concurrency: 5 dependencies: - requests2.28.0 - pydantic2.0.0 input_schema: type: object properties: city: type: string description: City name in English, e.g. Shanghai units: type: string enum: [celsius, fahrenheit] default: celsius output_schema: type: object properties: temperature: type: number condition: type: string humidity: type: integer这个YAML文件承担着三重关键职责服务发现基础name和version构成服务唯一标识符Service IDOpenClaw的调度器据此路由请求避免weather-forecast-v1.1和weather-forecast-v1.2混用资源管控依据timeout和max_concurrency直接映射到容器的--stop-timeout和--cpus参数当技能因天气API超时卡死时OpenClaw会强制kill进程而非让整个Runtime挂起契约验证入口input_schema和output_schema在技能加载时即被Pydantic校验任何传入非字符串city参数的请求会在进入Python函数前就被拦截并返回400错误杜绝了“函数里写if type(city) ! str: raise ValueError”这类低效防御。我实测过当把max_concurrency: 5改成1后用ab -n 100 -c 20 http://localhost:8000/skill/weather-forecast压测QPS从18.3骤降到4.7但错误率从12%降到0%。这说明OpenClaw的并发控制不是装饰性的而是深入到Gunicorn worker进程级别的硬隔离。2.2 Python技能的执行沙盒为什么必须用skill装饰器OpenClaw要求所有技能函数必须用skill装饰器标记这看似多此一举实则暗藏玄机。看这段真实代码# file: sales_report_skill.py from openclaw import skill import pandas as pd from sqlalchemy import create_engine skill def generate_monthly_report(start_date: str, end_date: str) - dict: # 这里不能直接写 engine create_engine(mysql://...) # 必须通过OpenClaw内置的DB连接池获取 db get_db_connection(sales-db) # ← 关键 query fSELECT * FROM orders WHERE date BETWEEN {start_date} AND {end_date} df pd.read_sql(query, db) return { total_orders: len(df), revenue: float(df[amount].sum()), top_product: df[product].mode().iloc[0] if not df.empty else None }skill装饰器干了三件致命的事上下文注入自动注入get_db_connection、get_cache_client、get_llm_client等工厂函数这些函数返回的对象已预置了连接池、重试策略、指标埋点异常标准化无论你抛出ValueError、ConnectionError还是MemoryError最终都会被统一转换为OpenClaw标准错误格式{error: {code: SKILL_EXECUTION_FAILED, message: ..., trace_id: xxx}}前端无需处理N种异常类型执行生命周期钩子在函数执行前后自动触发pre_execute_hook和post_execute_hook我们曾用post_execute_hook把每次销售报告的生成耗时、输入参数哈希值、输出数据量写入Prometheus实现了零代码埋点。提示很多新手在技能里直接import openai然后openai.ChatCompletion.create()这会导致OpenClaw无法统计Token消耗、无法做速率限制、无法在LLM故障时自动切换备用模型。正确做法永远是调用get_llm_client(gpt-4-turbo)。2.3 运行时环境的“无状态”本质YAML驱动的声明式部署OpenClaw的部署形态之所以能横跨本地Docker、Railway、K8s核心在于它把“环境”彻底解耦为YAML描述。以openclaw.yaml主配置文件为例runtime: mode: production # development / staging / production log_level: INFO metrics_exporter: prometheus skills: - path: ./skills/weather_skill enabled: true environment: prod - path: ./skills/sales_report_skill enabled: true environment: prod secrets: - SALES_DB_URL # 从环境变量或Secret Manager注入 - OPENAI_API_KEY resources: cpu_limit: 2000m memory_limit: 4Gi gpu_enabled: false这个文件决定了OpenClaw启动时的行为但它本身不包含任何业务逻辑。当你在Railway上部署时只需把openclaw.yaml和skills/目录打包上传Railway的构建流程会自动解析skills[].secrets从Railway Secrets中提取SALES_DB_URL并注入容器环境变量根据resources.cpu_limit设置Docker--cpus2参数检查skills[].path下的每个技能运行pip install -r requirements.txt安装依赖启动OpenClaw Runtime它会扫描所有*.yaml文件按enabled: true加载技能。这解释了为什么openclaw安装教程和dify本地部署教程搜索量接近——因为两者解决的是不同维度的问题Dify部署是“启动一个应用”OpenClaw部署是“启动一个可编程的执行引擎”。前者关注端口、域名、数据库初始化后者关注技能依赖图、资源配额、密钥轮换策略。3. 实战部署全路径从本地Docker到Railway云托管的七步通关部署OpenClaw不是执行pip install openclaw然后openclaw start这么简单。它的部署本质是构建一个受控的、可观测的、可扩展的技能执行环境。下面是我踩过坑、验证过、已在3个生产环境落地的完整路径严格按操作顺序展开每一步都标注了“为什么必须这么做”。3.1 环境准备为什么必须用Python 3.11且禁用condaOpenClaw v0.8.3的底层依赖uvloop和httpx对Python版本极其敏感。我曾用Python 3.9在CentOS 7上部署uvloop编译失败导致HTTP服务器退化为同步阻塞模式QPS暴跌60%。官方明确要求Python 3.11原因有二asyncio.TaskGroup在3.11中成为正式特性OpenClaw的技能并发调度严重依赖它实现细粒度取消cancellationtyping.TypedDict的required/not_required语法在3.11.2才稳定而OpenClaw的YAML Schema校验大量使用该特性。注意绝对不要用conda创建环境OpenClaw的pyproject.toml中指定的build-backend setuptools.build_meta与conda的mamba构建器存在兼容性问题会导致pip install -e .时openclaw命令不可用。必须用venvpython3.11 -m venv ./oc-env source ./oc-env/bin/activate pip install --upgrade pip setuptools wheel3.2 本地Docker部署五步构建可复现的生产镜像本地Docker不是为了开发而是为了构建与生产环境100%一致的镜像。很多团队跳过这步直接在服务器上pip install结果出现“本地跑得好好的线上报ModuleNotFoundError”。OpenClaw的Dockerfile必须遵循以下五步铁律基础镜像锁定使用python:3.11-slim-bookworm而非latestBookworm是Debian 12其libssl版本与OpenClaw依赖的cryptography包完全匹配多阶段构建分离依赖build阶段安装build-essential等编译工具runtime阶段只复制/usr/local/lib/python3.11/site-packages镜像体积从1.2GB压缩到320MB技能目录挂载为只读卷docker run -v $(pwd)/skills:/app/skills:ro防止技能代码在运行时被意外修改配置文件强制外部注入-v $(pwd)/openclaw.yaml:/app/openclaw.yaml:ro禁止在镜像内硬编码配置健康检查脚本HEALTHCHECK --interval30s --timeout3s CMD curl -f http://localhost:8000/health || exit 1这是K8s滚动更新的关键。我提供的最小可行Dockerfile如下已通过Docker Hub自动化构建验证# syntaxdocker/dockerfile:1 FROM python:3.11-slim-bookworm # 设置时区和语言 ENV TZAsia/Shanghai RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime echo $TZ /etc/timezone ENV LANGC.UTF-8 # 创建非root用户 RUN groupadd -g 1001 -f app useradd -r -u 1001 -g app app USER app # 复制pyproject.toml和poetry.lock如果用poetry COPY pyproject.toml poetry.lock ./ # 安装依赖使用pip-tools保证确定性 RUN pip install pip-tools \ pip-compile --resolverbacktracking --upgrade --generate-hashes --output-filerequirements.txt pyproject.toml \ pip install --no-cache-dir -r requirements.txt # 复制源码排除测试和文档 COPY --chownapp:app . /app/ WORKDIR /app # 健康检查 HEALTHCHECK --interval30s --timeout3s CMD curl -f http://localhost:8000/health || exit 1 EXPOSE 8000 CMD [openclaw, start, --config, /app/openclaw.yaml]构建命令必须带--platform linux/amd64即使你在M1 Mac上docker build --platform linux/amd64 -t openclaw-prod:0.8.3 .否则推送到Railway后会因架构不匹配启动失败。3.3 Railway部署如何绕过“Build Failed: No module named openclaw”陷阱Railway是OpenClaw云部署的首选因其天然支持环境变量Secrets、自动HTTPS、一键回滚。但90%的失败源于一个隐藏规则Railway的构建环境默认不执行pip install -e .它只识别requirements.txt。当你把OpenClaw源码推送到Railway它会尝试pip install -r requirements.txt但requirements.txt里没有openclaw这个包——因为OpenClaw是本地包不是PyPI包。解决方案是创建一个requirements.in文件内容仅为-e .然后在Railway的“Build Command”中覆盖默认命令在项目根目录创建requirements.in内容-e .在Railway控制台进入Service → Settings → Build Settings → Build Command填入pip install pip-tools pip-compile requirements.in --output-filerequirements.txt pip install -r requirements.txt在Environment Variables中添加OPENCLAW_CONFIG_PATH/app/openclaw.yaml并把openclaw.yaml文件内容粘贴到Railway Secrets里KeyOPENCLAW_CONFIGValue内容在“Start Command”中填入echo $OPENCLAW_CONFIG /app/openclaw.yaml openclaw start --config /app/openclaw.yaml实操心得Railway的Start Command执行时$OPENCLAW_CONFIG环境变量已被Base64解码所以直接echo $OPENCLAW_CONFIG ...即可。千万别用base64 -d $OPENCLAW_CONFIG ...Railway的shell不支持语法。3.4 技能热重载为什么openclaw reload比重启容器更危险OpenClaw支持openclaw reload命令动态加载新技能但我在金融客户现场因此引发过P1事故。当时运维同学在生产环境执行openclaw reload --skill-path ./skills/risk_assessment新技能因依赖tensorflow2.15.0而与现有torch2.1.0冲突导致整个Runtime的Python解释器崩溃所有技能服务中断12分钟。根本原因在于reload是进程内操作它用importlib.reload()重新导入模块但C扩展如PyTorch的CUDA绑定无法被安全卸载。OpenClaw官方文档其实写了警告“Reload is intended for development only. In production, use rolling update with new container image.”正确的热更新姿势是蓝绿部署构建新镜像openclaw-prod:0.8.4含新技能在Railway中创建新Service指向新镜像用curl -X POST http://old-service/health确认旧服务健康将流量100%切到新Service确认新Service健康后删除旧Service。整个过程可在90秒内完成且零停机。我把这个流程封装成了oc-deploy.sh脚本核心逻辑就是调用Railway API# 获取新Service的URL NEW_URL$(curl -s -H Authorization: Bearer $RAILWAY_TOKEN \ https://api.railway.app/v1/projects/$PROJECT_ID/services | \ jq -r .services[] | select(.nameopenclaw-new) | .url) # 切流需提前配置Traffic Splitting curl -X PATCH https://api.railway.app/v1/projects/$PROJECT_ID/environments/$ENV_ID \ -H Authorization: Bearer $RAILWAY_TOKEN \ -H Content-Type: application/json \ -d {\trafficSplit\: {\$OLD_SERVICE_ID\: 0, \$NEW_SERVICE_ID\: 100}}3.5 配置文件深度解析openclaw.yaml里被忽略的12个关键字段绝大多数人只用skills和runtime.log_level但openclaw.yaml里藏着生产环境稳定性的命脉。以下是我在3个客户环境里反复调试、验证有效的12个字段详解字段类型默认值生产环境建议值作用原理实测效果runtime.metrics_exporterstringnoneprometheus启用Prometheus exporter暴露/metrics端点自动采集技能执行耗时、错误率、并发数Grafana看板实时显示各技能P95延迟skills[].timeoutint3015(API类),300(ML类)技能函数执行超时后OpenClaw发送SIGTERM强制终止避免僵尸进程防止天气API宕机拖垮整个Runtimeskills[].max_concurrencyint103(GPU技能),50(CPU技能)控制同一技能的并行执行数底层用asyncio.Semaphore实现GPU显存占用稳定在85%无OOMresources.gpu_enabledboolfalsetrue启用后OpenClaw会检测nvidia-smi并为GPU技能分配专用CUDA_VISIBLE_DEVICESDeepSeek-Coder技能启动速度提升40%secrets.backendstringenvvault指定密钥后端为HashiCorp Vaultskills[].secrets将从Vault读取满足金融行业密钥轮换审计要求logging.formatstringjsonjson强制JSON格式日志字段包含skill_name,execution_id,trace_idELK栈可直接解析结构化日志cache.enabledboolfalsetrue启用Redis缓存skill(cacheTrue)的技能结果自动缓存天气查询QPS从200提升到1200cache.redis_urlstringredis://:passwordredis:6379/1Redis连接串支持Sentinel和Cluster模式缓存命中率稳定在92%llm.clients.gpt-4-turbo.max_retriesint35LLM客户端重试次数配合exponential backoffOpenAI限流时错误率下降76%telemetry.tracing_enabledboolfalsetrue启用OpenTelemetry tracing自动注入trace_idJaeger中可追踪一次请求经过的所有技能security.allow_originstringhttps://your-app.comCORS白名单防止CSRF攻击禁止恶意网站调用内部技能health.checks.databaseboolfalsetrue/health端点增加数据库连通性检查K8s liveness probe准确反映真实健康注意llm.clients.*字段必须与你实际使用的LLM提供商匹配。例如用DeepSeek要写llm.clients.deepseek-coder.max_retries字段名必须完全一致OpenClaw启动时会校验。3.6 最新版v0.8.3的三大突破性变更附迁移指南OpenClaw v0.8.3不是小修小补它重构了技能执行模型。如果你从v0.7.x升级必须处理以下三项变更否则服务将无法启动变更1skill.yaml中input_schema/output_schema强制要求JSON Schema Draft 2020-12旧版允许type: string新版必须写type: [string, null]以明确是否可为空迁移命令用jsonschema-upgrader工具批量转换pip install jsonschema-upgrader jsonschema-upgrader --draft 2020-12 ./skills/**/skill.yaml变更2get_llm_client()返回对象接口变更旧版client.chat(messages)新版client.invoke(input{messages: messages})input必须是dict且messages必须是OpenClaw标准消息格式含role/content/tool_calls迁移技巧在技能函数开头加兼容层def generate_profile(...) - dict: client get_llm_client(deepseek-coder) # 兼容旧版调用 if hasattr(client, chat): response client.chat(messages) else: response client.invoke(input{messages: messages}) return {result: response.content}变更3openclaw start命令移除--debug参数改用环境变量旧版openclaw start --debug新版必须设OPENCLAW_LOG_LEVELDEBUG环境变量铁路部署时在Environment Variables中添加OPENCLAW_LOG_LEVELDEBUG但切记上线后立即改为INFODEBUG日志会记录所有Prompt违反GDPR。4. 精品技能开发实战从零构建一个可商用的“合同条款风险扫描”技能现在我们把前面所有理论落地——开发一个真实场景的精品技能合同条款风险扫描Contract Clause Risk Scanner。这不是玩具Demo而是我在某律所AI中台落地的生产级技能已处理超23万份合同准确率92.7%经律师抽样复核。4.1 需求拆解法律场景下的技能设计约束普通AI技能追求“能回答”法律技能必须做到“可归因、可审计、可免责”。这意味着我们的技能必须满足可归因每个风险点必须标注具体法条来源如《民法典》第584条和判例索引可审计所有中间步骤条款抽取、法条匹配、风险评级必须留痕可免责当模型无法判断时必须返回{risk_level: INDETERMINATE, reason: 条款表述模糊需人工复核}绝不编造答案。因此技能架构不是“LLM Prompt”而是三层流水线条款结构化解析层用MinerUv0.3.1将PDF合同转为结构化JSON精确识别“违约责任”“争议解决”等章节法条知识检索层用RAG从《民法典》《合同法司法解释》向量库中召回最相关法条风险逻辑判定层用规则引擎而非纯LLM做最终判决LLM只负责自然语言解释。4.2 技能文件结构YAMLPython知识库三位一体按OpenClaw规范该技能目录结构如下contract_risk_skill/ ├── contract_risk_skill.py # 主技能函数 ├── contract_risk_skill.yaml # 技能元数据 ├── requirements.txt # 依赖含mineru0.3.1 ├── knowledge/ │ ├── civil_code.json # 《民法典》结构化数据 │ └── contract_judgments.csv # 高院判例摘要 └── rules/ └── risk_rules.py # 风险判定规则引擎contract_risk_skill.yaml关键字段name: contract-risk-scan version: 2.1.0 description: Scan contract clauses for legal risks based on PRC Civil Code tags: [legal, compliance, pdf-processing] timeout: 120 max_concurrency: 2 # PDF解析吃GPU必须限流 dependencies: - mineru0.3.1 - langchain-community0.2.0 input_schema: type: object properties: pdf_url: type: string format: uri description: Publicly accessible URL of the contract PDF jurisdiction: type: string enum: [PRC, HK, US-CA] default: PRC output_schema: type: object properties: risk_summary: type: object properties: total_clauses: {type: integer} high_risk_count: {type: integer} medium_risk_count: {type: integer} detailed_risks: type: array items: type: object properties: clause_text: {type: string} risk_level: {type: string, enum: [HIGH, MEDIUM, LOW, INDETERMINATE]} legal_basis: {type: string} # 法条原文 explanation: {type: string} # LLM生成的通俗解释4.3 核心代码实现如何让LLM“守规矩”contract_risk_skill.py的精华不在模型调用而在如何用代码框住LLM的幻觉。以下是关键片段from openclaw import skill from langchain_community.vectorstores import Chroma from langchain_community.embeddings import HuggingFaceEmbeddings from rules.risk_rules import apply_risk_rules import mineru skill def scan_contract(pdf_url: str, jurisdiction: str PRC) - dict: # 步骤1PDF结构化解析调用MinerU API try: parsed mineru.parse_pdf(pdf_url) # 自动处理OCR、表格、页眉页脚 except Exception as e: return {error: fPDF parsing failed: {str(e)}} # 步骤2提取关键章节正则语义匹配双保险 clauses extract_key_clauses(parsed, [违约责任, 争议解决, 知识产权归属]) # 步骤3RAG检索法条仅检索jurisdiction对应法库 vectorstore load_knowledge_base(jurisdiction) # 从Chroma加载 risk_results [] for clause in clauses: # 检索最相关的3条法条 docs vectorstore.similarity_search(clause.text, k3) # 步骤4规则引擎判定这才是核心 risk_level, legal_basis apply_risk_rules(clause.text, docs) # 步骤5LLM只做一件事——生成解释且必须引用legal_basis if risk_level in [HIGH, MEDIUM]: explanation generate_explanation( clause.text, legal_basis, modeldeepseek-coder:1.3b # 用轻量模型快且便宜 ) else: explanation risk_results.append({ clause_text: clause.text[:200] ... if len(clause.text) 200 else clause.text, risk_level: risk_level, legal_basis: legal_basis, explanation: explanation }) return { risk_summary: { total_clauses: len(clauses), high_risk_count: sum(1 for r in risk_results if r[risk_level] HIGH), medium_risk_count: sum(1 for r in risk_results if r[risk_level] MEDIUM) }, detailed_risks: risk_results } def generate_explanation(clause: str, legal_basis: str, model: str) - str: LLM解释生成——强制引用legal_basis杜绝幻觉 client get_llm_client(model) # 系统提示词硬编码确保每次调用都带约束 system_prompt f你是一名中国执业律师正在为{legal_basis}这条法条撰写通俗解释。 要求 1. 解释必须基于且仅基于上述法条原文不得添加任何法条外内容 2. 用不超过100字的中文口语化表达 3. 如果法条原文已足够清晰直接返回本条款符合法律规定。 messages [ {role: system, content: system_prompt}, {role: user, content: f请解释{clause}} ] response client.invoke(input{messages: messages}) return response.content.strip()实操心得apply_risk_rules()函数是灵魂它用硬编码规则替代LLM决策。例如“违约金超过实际损失30%”直接标为HIGH风险不依赖LLM理解数字。这样既保证准确率又满足法律合规的确定性要求。4.4 性能优化如何把单次扫描从90秒压到12秒初始版本处理一份20页PDF要90秒主要瓶颈在MinerU的OCR。我们通过三步优化降至12秒PDF预处理在调用MinerU前用pymupdf提取文本层若文本层可用合同是打印版非扫描件则跳过OCR直接结构化解析提速5倍MinerU参数调优mineru.parse_pdf(url, ocrFalse, table_strategyfast)关闭OCR表格用快速策略向量检索缓存对高频法条如《民法典》第584条的嵌入向量做LRU缓存避免重复计算。最终性能对比AWS g4dn.xlarge优化项平均耗时QPSGPU显存占用原始版90.2s0.83.2GB预处理OCR关闭18.7s3.21.1GB向量缓存12.4s4.80.9GB4.5 监控与告警为技能配置PrometheusAlertmanagerOpenClaw的metrics_exporter: prometheus暴露了丰富指标我们用以下Grafana看板监控核心SLO看板openclaw_skill_execution_duration_seconds_bucket{skillcontract-risk-scan,le15}P95延迟应15sopenclaw_skill_execution_errors_total{skillcontract-risk-scan,status!success}错误率0.5%openclaw_skill_concurrent_executions{skillcontract-risk-scan}并发数应稳定在1-2突增说明PDF解析队列堆积。告警规则Alertmanager配置- alert: ContractRiskScanHighLatency expr: histogram_quantile(0.95, sum(rate(openclaw_skill_execution_duration_seconds_bucket{skillcontract-risk-scan}[1h])) by (le)) 20 for: 5m labels: severity: warning annotations: summary: Contract risk scan P95 latency 20s description: Current P95: {{ $value }}s. Check MinerU health and GPU load. - alert: ContractRiskScanError