教授编码代理在Amazon SageMaker上部署生产端点
Agents everywhere, and we want to ensure that the Hugging Face Hub is a great home for them! So one of my first tasks after joining Hugging Face is to make sure coding agents actually succeed when they reach for our cloud partner integrations, and here you can find a write-up on what I found as I went through it along with agents. This post is about what happened when I asked the most popular coding agents to deploy an LLM on Amazon SageMaker. In the next posts I'll show you how to teach your agents to do real AI work on your favorite cloud. Follow me to catch them when they land!
将Hugging Face模型部署到SageMaker大多是重复性工作。不同模型之间的步骤几乎不变,这正好适合交给编码代理。理论上,你指点一个代理到Hub上的模型ID,走开,回来时就能得到一个工作的端点。我想看看实际情况如何,于是我把任务交给了运行Opus 4.8的Claude Code——根据Hub自己的代理使用数据,这是目前大多数人用来与Hugging Face合作的代理。我全程观察运行过程,而不仅仅是最后检查端点。剧透:进展不顺利。
第一个惊喜是代理甚至不会以相同的方式失败两次,而且失败的严重程度与模型的新旧程度相关。当我让它部署一个小型文本生成模型(Qwen/Qwen3-0.6B)时,它给了我一个能工作但脆弱的端点。注意,这个模型自2025年4月就在Hub上,写作时已超过一年,在这个领域已经算老模型了,所以这是个简单案例。当我让它部署google/diffusiongemma-26B-A4B-it(一个2026年6月发布的多模态扩散模型,不到一个月)时,它交回了一个自信的部署脚本,但脚本构建在一个根本无法运行这类模型的容器上,留给我自己去发现。模型越新,与代理训练数据中的任何内容距离越远,自信的猜测就越糟糕。
不可预测性才是真正的问题。如果知识没有写在代理可以访问的地方,即使是一个强大的模型,每次也会给你不同的、不同方式出错的结果。同一个代理,在我给它一组我编写的技能后,对每个我抛给它的模型表现一致。我故意选了一个很强的基线:如果前沿模型自己都这么摇摇晃晃,更小或更老的模型不会做得更好,所以以下情况更接近最佳情况而非最差情况。
文本生成运行是最清晰的可讲解案例,因为默认代理确实达到了一个实时端点。这让我们可以把两次运行并列对比,逐行对照。请求是那种团队成员实际会发送的内容:
我需要在上部署一个模型 AWS SageMaker,我不想处理所有的控制台点击和boto3。模型是一个小的Qwen,从内部应用调用。找出部署它的最佳方法并向我说明。先把计划写到一个文件里,并记录每一个操作步骤。
模型是Qwen/Qwen3-0.6B,部署到单个ml.g5.xlarge实例上的实时端点。以下是我讲解之前两次运行产生的结果:
默认代理
带技能
服务容器
先TGI → 健康检查失败 → vLLM
vLLM,在创建任何资源之前就已选择
镜像URI
通过试错发现
从AWS的DLC目录解析,当ECR被拒绝时回退
自动扩缩
无
目标追踪,1–2个实例
监控
无
三个CloudWatch告警(延迟、错误、开销)
留下的文档
README推荐TGI、SageMaker SDK、Python 3.13
计划和脚本与实际运行一致
区域/角色/环境
正确,凭直觉
正确,按规则
清理
一个可以运行的脚本
运行,然后验证资源已被删除
本篇文章的其余部分将介绍右侧列是如何构建的,以及为什么两者之间的差距与模型的智能程度几乎没有关系。
第一次运行出错的地方
平心而论,默认代理在没有提示的情况下做了很多正确的事。它读取了本地AWS配置来查找账户和区域,而不是问我。它先写了一个计划,等待后再动手。它尝试寻找现有的执行角色,而不是尝试创建新角色。它保持了每条命令的干净日志。如果你仅根据它最终得到的端点来评判,你会认为这是成功的。
问题出在它如何达到那里,以及它留下了什么。
它选择了TGI来服务模型。这是一个可以理解的选择。TGI是Hugging Face自家的文本生成服务器,多年来它是在SageMaker上服务Hub LLM的默认方式,因此代理的训练数据中充斥着使用TGI的教程。问题在于TGI后来被归档了,该区域可用的构建版本早于Qwen3架构,因此端点健康检查失败。自那以后,我们开始发布Hugging Face版本的vLLM、SGLang和llama.cpp。理想情况下,代理应该能在这里找到我们最新的所有容器,并知道哪个对该模型效果最好。代理没有尝试一次就放弃。它升级了TGI版本,重新部署,再次失败,大约80分钟和四次失败的端点尝试后才切换到vLLM,每次尝试都会在启动然后崩溃时消耗GPU时间。
`
10:22:28 [aws] FAILURE endpoint=qwen3-06b-endpoint reason=ping-health-check-failed
10:22:28 [claude] HYPOTHESIS root_cause=TGI-3.0.1-predates-Qwen3-arch-support remediation=bump-to-3.3.6
11:14:19 [claude] PIVOT backend=TGI->vLLM reason=TGI-too-old-for-Qwen3-confirmed-via-console-logs`
切换到vLLM发生在第一次健康检查失败后52分钟;从第一次TGI尝试算起,整个绕路大约消耗了80分钟。而为当前的Qwen选择TGI并不需要实验才能决定的判断。在今天这是一个已知事实,那80分钟是代理没有掌握这个知识的代价。
README.md更糟糕。实际运行的脚本最终使用vLLM和纯boto3,但README告诉读者使用TGI,安装SageMaker Python SDK,并保证Python 3.13没问题。其中每一条都是破坏SageMaker部署的可靠方法,而其中一条正是代理刚刚花了一小时搞定的失败。文档直接把下一个人指向了同一堵墙。
端点本身能工作,但那只是一个演示。一个始终运行的单个GPU实例,没有自动扩缩,没有告警。由于实时端点没有缩到零的功能,它在存在期间始终按完整实例费率计费,无论它是在服务流量还是闲置运行。一个按需的ml.g5.xlarge每月大约花费730美元,部署中没有任何东西会停止计费。关闭它是一个你必须记得的手动步骤。
那是明显的失败。第二个请求,针对google/diffusiongemma-26B-A4B-it(一个多模态混合专家模型,通过离散扩散而不是逐个token生成)悄然失败。代理查找了模型,确认它存在,然后写了一个部署脚本,再次选择了TGI:
`
image_uri=get_huggingface_llm_image_uri("huggingface", version="3.0.1") # TGI DLC`
TGI是一个文本生成服务器;它没有适用于离散扩散图像-文本模型的后端。代理隐约知道这一点。它同时注意到“默认TGI可能没有该后端”以及它选择的实例是“一个4B活跃大小的猜测”。它交给我一个自信的脚本,但构建在错误容器上,把验证留给我。没有像Qwen健康检查那样爆炸的事情发生。你只会在端点拒绝启动时才发现。
两次运行是同一根本原因的不同形式:一个模型上大声死亡,另一个模型上悄然错误。两者都不是推理失败。代理的计划和调试都很好。它缺少的是当前、特定于任务的知识:TGI已被归档,最近的Qwen模型需要vLLM,Python 3.13对大部分栈没有可用的wheel,容器镜像应该来自AWS发布的目录而不是模型的记忆。这类知识不能整齐地放在模型权重中。权重里存放的是它读过的一切的平均值,而它读过的大量内容是较早的教程,仍然说“使用TGI”和“pip install sagemaker”。此外,模型的知识有硬截止日期:新架构、新容器标签或新归档的服务器根本不在权重中。你要求代理处理的东西越新,它就越需要猜测,这正是为什么它处理一年前的Qwen比处理一个月的DiffusionGemma更好。即使代理做对的事情,它也只是在这个账户、这一天、没有任何保证相同调用会在更严格的账户或不同模型上有效的情况下做对了。区别从来不是它推理的能力有多强。它只取决于它可用的是什么。
将知识写成技能
修复方法不是更大的模型。而是当模型需要时,准确地把知识交给它的方法。我使用了技能:Markdown文件,每个都有名称、描述(告诉代理何时加载它)和指令主体,有时附带辅助脚本。技能在任务匹配其描述时按需加载,这样模型保持通用,而过程保持在版本控制中。
以下是修复容器问题的一个技能的精简版本:
`
---
name: serving-image-selection
description: >
Pick the right serving container for a SageMaker deployment and find its
current image URI. Use whenever an image URI needs to be chosen. Prefers the
HuggingFace-curated DLCs first: HuggingFace vLLM (LLMs), vLLM-Omni
(multimodal), TEI (embeddings/rerankers), HF Inference Toolkit, with
DJL-LMI and SGLang as alternatives. Never hardcode a container URI from
memory and never default to TGI.
---
Serving Image Selection
The serving container is the single thing most likely to break a deployment that
"looked correct on paper". Wrong container, stale tag, or wrong AMI all produce
the same opaque Failed to pass health check error.
Where image URIs come from
Primary source: AWS's official Deep Learning Containers catalog. Read the URI
from there, substitute the region, and pass it to deploy.py --image-uri. Never
recite a tag from memory.
Worth noticing: most of the families this skill chooses between are Hugging Face's own serving stacks, published as AWS Deep Learning Containers through the HF-AWS partnership. TEI (Text Embeddings Inference) for embeddings and rerankers, the HF Inference Toolkit for classic pipelines, HF-curated builds of vLLM and SGLang. The right container for a Hub model usually already exists. The agent's job is just to pick it on purpose instead of reciting one from memory.`
我编写了六个技能,覆盖了端到端部署,代理随着任务经过不同阶段时拉入它们:
`
sagemaker-deployment-planner decide pathway (real-time / async / batch), ask only what's needed
│
├── aws-context-discovery profile, region, account, caller identity (read-only)
├── python-env-setup isolated env, supported Python, current boto3
├── sagemaker-iam-preflight find a usable execution role; create only if none exists
├── serving-image-selection pick the container family, resolve a current image URI
└── sagemaker-production-defaults deploy with autoscaling + alarms + tags, then smoke-test`
第二次运行,逐步解析
有了技能后,相同的请求产生了非常不同的轨迹。这些行直接来自代理保留的操作日志:
`
STEP 4 (serving image selection) START. Family=vllm (NOT TGI; Qwen3 requires vLLM DLC).
NOTE: ecr-public:DescribeImages DENIED for SSO role -> resolver used known-good FALLBACK tag.
IMAGE_URI = 763104351884.dkr.ecr.us-east-1.amazonaws.com/vllm:0.21.0-gpu-py312-cu130-ubuntu22.04-sagemaker-v1.4
AWS CALL: CreateModel -> "qwen3-06b"
AWS CALL: CreateEndpoint -> "qwen3-06b-20260529-0722". Status=Creating. BILLING ACTIVE.
RESULT: Endpoint reached InService after 483s (~8 min).
AWS CALL: RegisterScalableTarget + PutScalingPolicy -> autoscaling min=1 max=2, target 20 invocations/min/instance.
AWS CALL: PutMetricAlarm x3 -> CloudWatch alarms (latency/errors/overhead).
AWS CALL: InvokeEndpoint -> HTTP 200, ~1.9s. Model=Qwen/Qwen3-0.6B, server=vllm-0.21.0.
NOTE: Qwen3 reasoning/think mode is ON by default; response hit max_tokens(64) inside <think>.`
容器问题在创建任何东西之前就解决了。serving-image-selection选择了vLLM(如果是嵌入模型,它会选择TEI;同样的技能也会引导扩散模型远离TGI),并从目录而不是从记忆解析了镜像。注意,权重从未经过我的笔记本电脑或S3:容器在端点启动时直接从Hub拉取Qwen/Qwen3-0.6B,如果需要,只需在下面的变量旁边加上HUGGING_FACE_HUB_TOKEN即可。模型随vLLM DLC和这些环境变量一起发布,技能知道vLLM容器期望这些变量:
`
SM_VLLM_MODEL=Qwen/Qwen3-0.6B
SM_VLLM_TRUST_REMOTE_CODE=true
SM_VLLM_MAX_MODEL_LEN=32768
SM_VLLM_GPU_MEMORY_UTILIZATION=0.9`
糟糕的文档不可能再次发生,因为python-env-setup编码了第一次运行记录的反面:每次都隔离环境,ML wheel实际存在的Python版本(3.10–3.12,从不3.13+),以及最新的boto3。它还默认直接使用boto3驱动部署,而不是通过SageMaker SDK。这是一个偏好,而不是硬性禁止:我们最近合并了一个PR,使SDK的路由逻辑与这些技能期望的对齐,所以SDK路径现在也很好用。但直接管理调用仍然让你对实际创建的内容拥有最紧密的控制,所以这是技能默认的路径。
执行角色是新手部署最常失败的地方,因为直接调用iam:CreateRole在企业账户上会失败,因为其SSO会话没有IAM写权限。sagemaker-iam-preflight颠倒了顺序。它的check_role.py搜索账户中匹配AmazonSageMaker-ExecutionRole-和SageMakerExecution模式的现有角色,按最后使用日期排序,验证信任策略,并打印ARN。只有当确实没有角色并且调用者有权限时才创建角色。代理找到了一个可用的角色,然后继续。
最后,sagemaker-production-defaults将端点从演示转变为你可以在生产环境中运行的东西。它一起创建模型、端点配置、端点、自动扩缩策略和告警,使用在同一个地方定义的默认值:
| 防护栏 | 设置 |
|---|---|
| 自动扩缩 | SageMakerVariantInvocationsPerInstance 的目标追踪,目标 20/实例,最小 1 / 最大 4 |
| 扩缩冷却时间 | 60s 出,300s 进 |
| 告警:ModelLatency | > 30,000 ms |
| 告警:Invocation5XXErrors | 在5分钟窗口内 > 5 |
| 告警:OverheadLatency | > 2,000 ms |
这些是技能的默认值。由于这是一个小模型,用于低流量内部应用,代理将自动扩缩上限为两个实例,而不是默认的四个,这就是为什么上面的日志显示max=2。
在进入任何计费内容之前,代理停下来请求明确的许可。当我完成后,它运行了清理,然后确认端点、配置和模型确实被删除,而不是假设删除成功。
我没有编程的部分
第二次运行中有两个时刻不是我设计的,但它们是整个练习中最有说服力的部分。
第一个是权限错误。为了找到最新的镜像标签,代理尝试直接询问ECR,但它运行的SSO角色没有调用ecr-public:DescribeImages的权限,因此调用被拒绝。一个天真的脚本会在那里崩溃。相反,代理回退到技能附带的已知良好标签作为安全网,并在日志中写下解释为什么这样做,然后继续到一个工作的端点。
第二个是质量捕捉。端点启动后,代理运行了一个烟雾测试来确认它实际响应。回复被截断。它达到了64个令牌的限制,而Qwen3仍在推理块内,所以“真实”答案从未输出。代理没有将测试标记为通过并继续,而是注意到了截断,理解了原因,并标记调用应用应该提高其令牌限制。
没有人告诉代理优雅地处理ECR调用的拒绝,或者对烟雾测试的内容进行合理性检查。技能只是给它提供了足够的关于这个部署应该如何运行的上下文,以至于它能够识别出什么时候不对劲,并自行做出明智的决策。
为什么六个技能而不是一个长提示
我本可以将所有这些塞进一个系统提示中。我试过,但行不通。一个足够长以覆盖每个尖锐边缘的提示会被部分忽略,一旦AWS发布新容器就会过时,而且必须被重新粘贴到每个会话中。技能只在其相关时加载,存在于版本控制中,位于它们调用的辅助脚本旁边,并且在一个地方更新。
最后一点实际上是全部论点。“对于Qwen3,使用vLLM,而不是TGI”在今天是真的,两年前不真,当时TGI是Hub LLM的默认服务栈。当下一个服务栈出现时,它又会改变。将一个这样的事实写进文件而不是相信模型会回忆起来的理由正是因为它将会改变。当它改变时,你编辑一个技能,而不是希望每个模型都读到了更新。辅助脚本是纯Python和AWS CLI,没有任何特定于Shell的内容,因此相同的技能在macOS、Linux和Windows上行为完全一致。
用你自己的Hub模型试试
本文中的技能在huggingface/skills中开源,作为hf-cloud-*技能。使用Hugging Face CLI安装它们:
`
hf skills add hf-cloud-aws-context-discovery
hf skills add hf-cloud-python-env-setup
hf skills add hf-cloud-sagemaker-deployment-planner
hf skills add hf-cloud-sagemaker-iam-preflight
hf skills add hf-cloud-serving-image-selection
hf skills add hf-cloud-sagemaker-production-defaults`
然后给你的代理一个这样的请求:
`
Deploy Qwen/Qwen3-0.6B to a real-time SageMaker endpoint for an internal app.`
重要的是请求保持普通。你不需要告诉代理使用哪个容器家族、如何找到SageMaker执行角色、如何解析DLC镜像URI、或者附加哪些生产默认值。这些决定都存在于技能中。
模型保持在Hub上,服务栈从Hugging Face和AWS集成中选择,部署时已经附带了生产组件:自动扩缩、告警、标签、烟雾测试和清理。
技能目前适用于Claude Code、Codex和Pi,Hugging Face Skills仓库通过hf skills使相同的指令可安装。因为它们只是Markdown文件加上小的辅助脚本,同样的模式可以适应其他支持加载任务特定指令的代理框架。如果你在未涉及的模型、区域、账户设置或服务路径上尝试它们,请提交issue或PR。关键是将这些知识保持公开,以便下次代理运行可以实际使用它。
要点
这个实验的教训不是代理不能部署模型,确实它们能。教训是部署知识变化的速度比模型权重快。
TGI曾经是SageMaker上许多LLM的明显答案。对于当前的Qwen模型,例如Qwen 3.6家族,它是错误的答案:它不再被维护,因此应该使用更新的、更新的解决方案。Python版本在ML wheel支持中进进出出。AWS容器标签在移动。企业IAM策略不同于教程账户。这些都不是困难的推理,但正是那种如果代理必须依赖过去知识就肯定会自信模糊的细节。而且效果随着事物更新而变得更强烈:代理在一个已经公开超过一年的模型上摇摇晃晃,在一个公开不到一个月的模型上完全错误。明天发布的任何东西距离其训练数据更远。
技能给这些知识一个家。它们让代理保持通用,而部署过程保持最新、可审核和可共享。对Hugging Face工作流来说,这很重要,因为Hub已经是模型的真相来源。下一步是使围绕该模型的操作知识同样可达。
一个能创建端点的编码代理很有用。一个知道使用哪个Hugging Face服务栈、验证它创建了什么、并留下你的团队可以操作的东西的编码代理才是我真正想要接近生产的那个。