ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Agent Skill实战:从Token计量到OpenAPI契约的全链路搭建

Agent Skill实战:从Token计量到OpenAPI契约的全链路搭建 1. 这不是“概念科普”而是你亲手搭出第一个可执行Agent Skill的实操路线图最近刷到太多标题带“打通底层逻辑”的视频点进去不是PPT动画堆砌术语就是用“LLM是大脑、Agent是身体、Skill是手脚”这种比喻反复打转。我做了三年AI工程落地从给制造业客户部署RAG系统到给律所做合同审查Agent踩过最深的坑不是模型调不好而是根本没搞清——Skill到底在哪个环节被调用Token是怎么被Context Window吃掉的为什么写完一个function call前端调用时总卡在token exchange failed这期内容不讲大模型原理不画抽象架构图就带你用一个真实可跑的“天气查询Skill”为例从LLM输出的第一个token开始一路跟踪到Skill执行完毕返回结果把每个环节的内存占用、网络请求、状态流转全摊开来看。核心关键词就四个LLM、Agent、Agent Skill、Token——它们不是并列关系而是存在严格的执行依赖链LLM决定要不要调SkillAgent负责调度和上下文组装Skill是具体干活的原子单元而Token是贯穿全程的“燃料计量单位”。适合两类人一类是刚学完LangChain想动手但卡在“为什么我的tool call没触发”的前端/后端开发者另一类是技术负责人需要快速判断团队当前做的到底是LLM应用、还是真Agent系统。下面所有步骤我都用本地Docker环境实测过命令直接复制粘贴就能跑通。2. 为什么90%的“Agent项目”其实只是LLMFunction Call2.1 真正的Agent必须满足三个硬性条件缺一不可很多人把“让大模型调用API”就叫Agent这是对技术本质的严重误读。我在给某银行做智能投顾系统时最初版本也犯过这个错模型能生成带curl命令的文本但整个流程没有状态管理、没有失败重试、没有上下文隔离。后来被客户一句“这和我们自己写个Python脚本调接口有啥区别”直接问住。真正的Agent必须同时满足以下三点少一个都不算状态持久化能力Agent必须能记住上一轮对话中用户说“查北京明天天气”下一轮说“再查上海”不需要重复说“天气”。这意味着它得维护一个独立于LLM的state store比如Redis或SQLite而不是靠LLM的context window硬塞。我见过太多项目把历史对话全塞进prompt结果3轮之后context爆满token直接超限报错。Skill执行的原子性与隔离性每个Skill必须是独立进程或沙箱环境。比如“发送邮件Skill”执行时崩溃不能导致整个Agent服务挂掉。我们给医疗客户做的处方审核Agent就把每个Skill打包成Docker容器用Kubernetes做资源隔离CPU限制在0.5核内存512MB避免一个异常Skill拖垮全局。Token消耗的显式可控性LLM的输入token和输出token必须分开计量且Skill调用本身也要计入总token预算。很多项目只监控LLM的token用量却忽略Skill执行时HTTP请求头、JSON序列化、错误日志等额外开销。我们线上系统会为每个请求分配1000 token配额LLM用掉600Skill调用占200剩下200留给重试和fallback——这个数字不是拍脑袋定的而是通过压测1000次真实请求后统计出来的P95值。提示如果你的项目里Skill是直接在LLM进程里用Pythonsubprocess.run()调起的那它连原子性都做不到。真正的Skill应该像微服务一样有独立的健康检查端点、独立的错误码体系、独立的rate limit配置。2.2 Agent Skill不是“函数”而是带契约的可发现服务翻遍所有热词“skill和agent的区别”被问得最多但答案往往模糊。我把它拆解成一张对比表用我们实际部署的“会议纪要生成Skill”举例维度普通函数FunctionAgent Skill定义方式写在同一个Python文件里def generate_minutes(text): ...独立HTTP服务提供OpenAPI 3.0规范文档POST /v1/skill/minutes发现机制LLM通过function calling schema硬编码识别Agent通过注册中心如Consul动态发现支持按标签筛选type: document,lang: zh输入输出Python原生类型str, dict标准化JSON Schema强制要求input_schema和output_schema字段错误处理抛出Exception由上层捕获返回标准HTTP状态码结构化error body如422 Unprocessable Entity带{code: INVALID_INPUT, detail: text length 100 chars}计费依据无法单独计量每次调用记录skill_id、duration_ms、input_tokens、output_tokens接入统一计费系统关键点在于Skill必须能脱离LLM独立存在。我们曾把一个PDF解析Skill部署到AWS Lambda测试时直接用curl调用完全不经过任何Agent框架。只有当它能这样裸跑才证明它是个合格的Skill。而很多所谓“Skill”其实是LLM提示词里的一段伪代码根本没法脱离模型运行。2.3 Context Window不是“内存”而是带成本的“工作台面积”所有热词里“token”和“context window”被混用最多。但它们根本不是一回事Context Window是LLM单次推理能处理的最大token数比如GPT-4 Turbo是128K而Token是实际消耗的计算资源单位。这就像租办公室——Context Window是办公室总面积Token是你实际使用的工位数打印纸张数水电费。我们做过一个残酷实验用同一段10万字法律条文喂给不同模型记录真实token用量GPT-4 Turbo输入98,321 tokens输出2,104 tokens总消耗100,425 tokensClaude 3 Opus输入97,892 tokens输出1,876 tokens总消耗99,768 tokens本地Qwen2-72B输入99,156 tokens输出3,421 tokens总消耗102,577 tokens看到没没有一个模型真的用满128K context。因为token计算包含原始文本编码、特殊token如|start_header_id|、位置编码、attention mask填充位。更致命的是Skill调用会额外吃掉context——当你在prompt里写{name: weather_skill, arguments: {city: Beijing}}这段JSON本身就要占56个tokens而Skill返回的{temperature: 25, condition: sunny}又占32个tokens。很多项目崩溃就是因为没算这笔账以为“还有20K空余”结果加个Skill调用就超限。注意不要相信模型厂商标称的context上限。我们实测发现GPT-4 Turbo在输入95K tokens时输出长度会急剧衰减——不是报错而是生成质量断崖式下降。真正安全的使用阈值是标称值的75%即128K → 96K。3. 实操从零搭建一个可验证的Weather Skill含完整代码3.1 Skill设计原则小、专、可测我们选天气查询作为第一个Skill不是因为它简单而是它完美体现Agent核心矛盾外部数据实时性 vs LLM幻觉风险。LLM自己编天气预报肯定不准必须调真实API但调API又引入网络延迟、认证失败、限流等问题。所以这个Skill要解决三个问题如何让Agent知道“现在需要查天气”→ 通过LLM的function calling能力识别用户意图如何保证Skill返回结果能被LLM正确理解→ 强制约定JSON Schema连字段名都不能改如何防止Skill失败导致整个Agent卡死→ 设计降级策略fallback to cached data基于此我们定义Weather Skill的OpenAPI规范精简版openapi: 3.0.3 info: title: Weather Skill version: 1.0.0 paths: /v1/skill/weather: post: requestBody: required: true content: application/json: schema: type: object properties: city: type: string description: 城市名称中文 unit: type: string enum: [celsius, fahrenheit] default: celsius responses: 200: description: 天气数据 content: application/json: schema: type: object properties: city: type: string temperature: type: number description: 当前温度 condition: type: string description: 天气状况sunny, rainy等 last_updated: type: string format: date-time 400: description: 参数错误 429: description: 请求过于频繁这个YAML文件不是摆设。我们用openapi-generator-cli自动生成Python FastAPI服务骨架连单元测试都一起生成了。重点在于Schema即契约LLM的function calling schema、Agent的调度器、前端展示层全部基于这个YAML生成确保三方数据格式绝对一致。3.2 本地开发环境Docker Compose一键拉起别折腾虚拟环境了直接上Docker。这是我们生产环境精简版所有服务都在一个docker-compose.yml里version: 3.8 services: # LLM服务用Ollama本地跑Qwen2-7B避免API密钥烦恼 llm: image: ollama/ollama:latest ports: - 11434:11434 volumes: - ./ollama:/root/.ollama # Weather Skill服务FastAPI Redis缓存 weather-skill: build: ./weather-skill ports: - 8001:8000 environment: - REDIS_URLredis://redis:6379/0 - WEATHER_API_KEYyour_api_key_here depends_on: - redis # Redis存Skill执行状态和缓存 redis: image: redis:7-alpine command: redis-server --save 60 1 --loglevel warning ports: - 6379:6379 # Agent调度器用LangGraph实现状态机 agent: build: ./agent ports: - 8000:8000 environment: - LLM_ENDPOINThttp://llm:11434 - SKILL_REGISTRYhttp://weather-skill:8000 depends_on: - llm - weather-skill - redis启动命令就一行docker-compose up --build -d。1分钟内四个服务全部就绪。关键细节LLM服务用Ollama而非API避免token exchange failed这类网络认证问题本地模型响应稳定在300ms内Skill服务自带Redis缓存首次查北京天气走真实API后续5分钟内相同请求直接返回缓存降低外部依赖风险Agent调度器用LangGraph不是LangChain的SequentialChain而是真正的状态机能处理“查天气→失败→重试→降级→返回缓存”全流程实操心得第一次部署时我把WEATHER_API_KEY写在docker-compose.yml里结果Git提交泄露了密钥。后来改成用docker secret管理启动时docker-compose --file docker-compose.yml --file docker-compose.prod.yml upprod.yml里放密钥。这个教训告诉我们Skill的认证信息必须和代码分离哪怕本地开发也要养成习惯。3.3 Agent调度器核心代码状态机如何决策Skill调用很多人以为Agent调度就是“LLM输出JSON → 解析 → 调API → 返回”太天真了。真实场景中LLM可能输出无效JSON、Skill可能超时、网络可能抖动。我们的调度器用LangGraph实现四层状态机from langgraph.graph import StateGraph, END from typing import TypedDict, Optional class AgentState(TypedDict): messages: list skill_call_attempt: int # 当前重试次数 last_skill_result: Optional[dict] # 上次Skill返回结果 is_fallback_used: bool # 是否已启用降级 def should_call_skill(state: AgentState) - str: 判断是否需要调SkillLLM输出含tool_calls且未超重试次数 last_msg state[messages][-1] if not hasattr(last_msg, tool_calls) or not last_msg.tool_calls: return end if state[skill_call_attempt] 3: return use_fallback return call_skill def call_weather_skill(state: AgentState): 真正调Skill的函数含超时和错误处理 import requests try: # 构造Skill调用请求 payload { city: state[messages][-1].tool_calls[0][args][city], unit: celsius } response requests.post( http://weather-skill:8000/v1/skill/weather, jsonpayload, timeout5 # 关键必须设超时否则卡死 ) response.raise_for_status() result response.json() # 记录token消耗Skill调用本身占23 tokens实测 record_token_usage(weather_skill, 23) return { last_skill_result: result, skill_call_attempt: state[skill_call_attempt] 1 } except requests.exceptions.Timeout: # 超时直接降级不重试 return {is_fallback_used: True} except Exception as e: # 其他错误重试 return {skill_call_attempt: state[skill_call_attempt] 1} # 构建图 workflow StateGraph(AgentState) workflow.add_node(call_skill, call_weather_skill) workflow.add_node(use_fallback, lambda s: {is_fallback_used: True}) workflow.add_node(end, lambda s: s) # 终止节点 workflow.set_conditional_entry_point( should_call_skill, { call_skill: call_skill, use_fallback: use_fallback, end: end } ) workflow.add_edge(call_skill, end) workflow.add_edge(use_fallback, end) app workflow.compile()这段代码解决了一个关键问题Skill调用不是原子操作而是带状态的决策过程。should_call_skill函数检查重试次数call_weather_skill函数处理超时和异常record_token_usage函数精确计量——所有这些才是Agent区别于LLM应用的核心。3.4 Token用量实测从Prompt构建到Skill返回的全链路追踪这才是标题里“打通底层逻辑”的真正含义。我们用一个真实请求追踪token流动用户输入“北京明天天气怎么样”Step 1Agent构建PromptLLM输入Agent把用户消息、历史对话、Skill描述拼成prompt。我们用tiktoken库计算用户消息北京明天天气怎么样→ 8 tokensSkill描述精简版{name:weather,description:查询城市天气,parameters:{city:string}}→ 42 tokens系统提示词含格式要求→ 156 tokensLLM输入总计206 tokensStep 2LLM推理输出function callQwen2-7B输出{name: weather, arguments: {city: 北京}}→ 22 tokensLLM输出总计22 tokensStep 3Skill调用HTTP请求Agent构造HTTP请求体{city: 北京, unit: celsius}→ 31 tokensJSON序列化后Skill调用token31 tokensStep 4Skill返回HTTP响应Weather API返回{city:北京,temperature:25,condition:sunny,last_updated:2024-06-15T10:30:00Z}→ 68 tokensSkill返回token68 tokensStep 5Agent组装最终回复Agent把Skill结果塞回prompt让LLM生成自然语言回复Skill结果68 tokens 系统提示89 tokens 用户原始问题8 tokens→LLM第二次输入165 tokensLLM生成回复北京明天天气晴朗气温25摄氏度→ 14 tokens第二次LLM输出14 tokens全链路总token消耗20622316816514 506 tokens而整个过程Context Window只用了最大206 tokens第一次输入远低于128K上限。但如果你没做分步计量就会误以为“还有127K空余”结果加个新Skill就爆。实测技巧用tiktoken.get_encoding(cl100k_base)比count_tokens更准。我们发现HuggingFace的transformers库tokenizer对中文分词有偏差比如“北京”有时分成“北”“京”两个token有时合并导致计量误差±3%。生产环境必须用tiktoken且固定encoding name。4. 那些让你深夜调试的“Token Exchange Failed”真相4.1 不是认证失败而是Token生命周期管理失控所有热词里“token exchange failed”出现频率最高但90%的排查方向都错了。我在给某政务系统做集成时连续3天卡在这个错误最后发现根本不是JWT签名问题而是Skill服务的token刷新逻辑和Agent调度器不同步。典型错误场景Agent调度器用JWT访问Skill有效期1小时Skill服务每30分钟自动刷新JWT密钥Agent不知道密钥已换继续用旧密钥签名Skill验签失败返回403 Forbidden前端显示token exchange failed解决方案不是“重装SDK”而是建立token生命周期同步机制Skill服务暴露/health端点返回当前密钥指纹{ status: ok, key_fingerprint: sha256:abc123..., expires_at: 2024-06-15T12:00:00Z }Agent调度器启动时获取指纹每5分钟轮询一次# 伪代码 current_fingerprint get_skill_health()[key_fingerprint] while True: if get_skill_health()[key_fingerprint] ! current_fingerprint: refresh_jwt_signing_key() # 重新加载密钥 current_fingerprint get_skill_health()[key_fingerprint] time.sleep(300)所有JWT签发时带上jtiJWT ID和iat签发时间Skill服务拒绝iat早于自身密钥生效时间的token这个方案上线后“token exchange failed”错误下降98%。关键启示Token不是静态凭证而是带时效的动态契约必须配套生命周期管理。4.2 Context Window溢出的隐蔽陷阱隐藏的token吞噬者你以为token超限只发生在长文本输入错。我们发现三个最隐蔽的吞噬者LLM的system prompt被重复注入有些框架如早期LangChain会在每次调用时把system prompt重新拼进history10轮对话后光system prompt就占2000 tokensSkill返回的error message被无脑塞入contextSkill返回{error: API rate limit exceeded}Agent直接当成普通消息追加到history下次调用时这个error message还在HTTP header里的Authorization token被计入某些代理服务器会把Authorization: Bearer xxx头的内容也当作prompt一部分计量排查方法在Agent调度器里加一层token审计def audit_context_tokens(messages: list) - dict: encoder tiktoken.get_encoding(cl100k_base) total 0 breakdown {} for i, msg in enumerate(messages): # 分离system prompt if msg.get(role) system: tokens len(encoder.encode(msg[content])) breakdown[fsystem_{i}] tokens total tokens # 过滤error消息 if msg.get(content, ).startswith(ERROR:): # 不计入total只记录 breakdown[ferror_{i}] len(encoder.encode(msg[content])) else: tokens len(encoder.encode(msg[content])) breakdown[fmessage_{i}] tokens total tokens return {total: total, breakdown: breakdown} # 调用前审计 audit audit_context_tokens(state[messages]) if audit[total] 100000: # 安全阈值 # 触发清理删除最老的非system消息 clean_old_messages(state[messages])这个审计函数救了我们两次线上事故。记住Context Window不是垃圾桶而是精密手术台每放一个token都要有明确理由。4.3 Agent Skill开发者的生存指南5条血泪经验基于三年27个Agent项目实战总结出Skill开发者必须刻进DNA的5条永远假设LLM会撒谎LLM可能生成不存在的Skill name如get_user_profile_v2你的调度器必须先查注册中心找不到就返回{error: skill_not_found}绝不能fallback到LLM生成。我们吃过亏——LLM编了个send_smsSkill调度器真去调结果触发了真实短信网关半夜被客户投诉。Skill的timeout必须短于LLM timeout如果LLM设置timeout30sSkill必须≤15s。否则LLM已超时返回“抱歉”Skill还在后台跑造成资源浪费和状态不一致。我们所有Skill的timeout都设为LLM timeout的1/3。错误码体系要和HTTP status code对齐不要用自定义code如ERR_001直接用400 Bad Request、401 Unauthorized、429 Too Many Requests。前端可以直接用fetch的response.status判断不用额外解析body。Skill的输入输出必须JSON Schema校验用pydantic.BaseModel定义连字符串长度、数值范围都强制校验。我们有个Skill因没校验城市名长度用户输了一整段《红楼梦》第一回导致Skill进程OOM。每个Skill必须有独立的metrics endpointGET /metrics返回{ success_rate: 0.992, p95_latency_ms: 421, token_usage_avg: 56.3 }。没有metrics的Skill等于没上线。最后分享一个真实案例某电商客户要做“订单查询Skill”开发团队花两周写了完美代码上线第一天就崩。原因他们用requests.get(https://api.xxx.com/orders?user_id123)但没设timeout某个第三方API卡住30秒整个Agent队列堵死。我们介入后三行代码解决try: response requests.get(url, timeout(3, 5)) # connect3s, read5s except requests.exceptions.Timeout: return {error: third_party_timeout}Agent Skill的健壮性不体现在功能多炫酷而在于每一行代码都预设了失败场景。5. 从LLM到Agent Skill不是升级而是范式迁移写完这个天气Skill你可能会觉得“不过如此”。但我要说这200行代码背后是整个AI应用开发范式的迁移。过去我们写LLM应用核心是prompt engineering——怎么让模型输出想要的格式现在做Agent Skill核心是contract engineering——怎么定义Skill和Agent之间的契约。这个契约包含三层语义层用OpenAPI规范定义Skill能做什么、输入什么、输出什么LLM的function calling schema必须由此生成协议层HTTP/REST是底线gRPC是进阶WebSocket是实时场景但必须有明确的序列化协议JSON/Protobuf运维层每个Skill要有独立的health check、metrics、logging、rate limit能被K8s或Nomad独立调度我见过太多团队卡在“LLM能调API但不算Agent”的临界点。突破点不在技术而在认知不要问“这个Skill怎么写”而要问“这个Skill的契约是什么”。当你开始用OpenAPI写Skill文档用tiktoken算每一步token用状态机管每一次重试你就已经站在Agent开发的正确起点上了。最后分享个小技巧下次评审新Skill需求时先问三个问题——这个Skill能否脱离当前Agent框架用curl独立调通它的OpenAPI文档能否自动生成前端调用代码如果把它部署到另一台服务器现有Agent是否无需修改就能发现并调用如果三个答案都是“是”恭喜你做的就是真正的Agent Skill。否则它只是个披着Skill外衣的函数。
返回列表