ARTICLE DETAIL

资讯详情

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

从API到Agent再到插件:客服工单分类Agent的三次造轮子实践

从API到Agent再到插件:客服工单分类Agent的三次造轮子实践 1. 三次造轮子的起因一个客服工单分类 Agent 的演进史去年底我接了个内部需求给客服团队做一个工单自动分类的小工具。需求本身不复杂读取工单文本判断它属于退款、物流、账号、产品咨询中的哪一类然后打上标签写回系统。团队里没人做过 AI 相关的功能我算是被赶鸭子上架。第一版我选了最直接的路子——直接调 API。理由很简单需求小、时间紧没必要为了一个分类功能引入一整套框架。写了个 Python 脚本把工单内容拼进 prompt调一次模型解析返回结果完事。整个脚本不到 80 行跑起来也确实能用。但问题很快来了。客服团队希望这个工具能接入他们日常用的编辑器让坐席在写工单的时候就能看到分类建议而不是切到另一个网页。于是有了第二版上 AgentCore把分类逻辑封装成一个可编排的 Agent 服务。再后来团队里几个工程师平时用 Claude Code 写代码他们希望分类能力能直接以插件形式嵌进 Claude Code 的工作流里于是有了第三版。同一个需求三种实现路径踩了三套完全不同的坑。这篇文章就把这三次实践完整拆开讲包括每次选型的理由、具体的实现步骤、遇到的典型问题以及我最后总结出来的选型判断标准。如果你也在纠结“一个 Agent 到底该直接调 API、上框架还是做成插件”这篇应该能帮你少走点弯路。2. 第一版直接调 API最快跑通也最容易翻车2.1 为什么第一版不选框架很多人一上来就想搭一套完整的 Agent 架构我建议先忍住。判断标准很简单如果你的任务是一次输入、一次输出、不需要多轮工具调用那就别上框架。工单分类恰好符合这个特征。输入是一段文本输出是一个类别标签中间不需要查数据库、不需要调外部工具、不需要多轮推理。这种场景下框架带来的抽象层全是负担——你要理解它的 Agent 定义、工具注册、状态管理最后发现核心逻辑还是那一次 API 调用。直接调 API 的另一个好处是调试成本极低。出问题了打印一下请求体和响应体问题基本就定位了。框架出问题你得先搞清楚是框架的哪一层出了问题再往下钻。2.2 核心实现一次 API 调用的完整链路我用的是 OpenAI 兼容格式的接口Python 环境依赖只有requests。核心逻辑分三步构造 prompt、发请求、解析结果。import requests import json API_URL https://your-api-endpoint/v1/chat/completions API_KEY sk-xxxxxxxx def classify_ticket(ticket_text): prompt f你是一个客服工单分类助手。请将下面的工单归类到以下类别之一 退款、物流、账号、产品咨询。 只输出类别名称不要输出其他内容。 工单内容 {ticket_text} payload { model: gpt-4o-mini, messages: [{role: user, content: prompt}], temperature: 0, max_tokens: 20 } headers { Authorization: fBearer {API_KEY}, Content-Type: application/json } resp requests.post(API_URL, jsonpayload, headersheaders, timeout30) resp.raise_for_status() result resp.json() return result[choices][0][message][content].strip()这段代码有几个关键点值得说。temperature 设为 0。分类任务要的是稳定输出不是创意。temperature 越高模型越可能给你输出“这看起来像是退款问题”这种带解释的句子解析起来就麻烦了。max_tokens 限制在 20。类别名称最多四个字给 20 个 token 绰绰有余。限制输出长度能防止模型“话痨”也能省点费用。prompt 里明确要求“只输出类别名称”。这是最容易被忽略的一点。你不说清楚模型很可能给你输出一段分析过程然后你还要写正则去提取。与其事后解析不如事前约束。2.3 踩坑记录那些让我半夜爬起来改代码的问题坑一401 报错key 明明是对的。我第一次跑的时候遇到unexpected status 401 unauthorized: incorrect api key provided: sk-svcac****。排查了半天发现是环境变量里多了一个换行符。从网页复制 key 的时候末尾带了个不可见的\n。这种问题特别隐蔽因为打印出来看着完全正常。解决办法是在代码里加一句API_KEY API_KEY.strip()养成习惯。坑二超长工单导致 400。有次坐席粘贴了一整段聊天记录进来触发了this models maximum context length is 1048576 tokens的报错。虽然这个上限看着很大但实际业务里确实会遇到超长输入。我的处理方式是在调用前做一次截断超过 8000 字符的部分直接砍掉因为工单的核心诉求通常在开头。坑三并发上来之后接口开始超时。客服高峰期同时有几十个工单进来同步请求直接排队。后来改成了批量处理一次请求塞多个工单让模型返回 JSON 数组。这里要注意批量处理时 prompt 的结构要更严格否则模型容易把多个工单的结果混在一起。def classify_batch(tickets): numbered \n.join([f{i1}. {t} for i, t in enumerate(tickets)]) prompt f将下列工单分别分类到退款、物流、账号、产品咨询。 以 JSON 数组格式返回每个元素只包含类别名称顺序与输入一致。 {numbered} # ... 请求逻辑同上解析时用 json.loads批量处理把 QPS 压力降了一个数量级代价是单次请求的 token 消耗变高需要权衡批次大小。我实测下来一批 10 个工单是比较稳的平衡点。3. 第二版上 AgentCore从脚本到服务的跨越3.1 什么时候该从 API 升级到 Agent 框架第一版跑了一个月需求开始变复杂。产品那边希望分类之后能自动触发后续动作退款类工单自动查订单状态物流类工单自动拉物流轨迹账号类工单自动检查账号是否被冻结。这就不是一次 API 调用能解决的了需要多步骤、多工具的编排。这时候 Agent 框架的价值才真正体现出来。判断标准当你的任务需要“根据中间结果决定下一步做什么”时就该考虑 Agent 框架了。工单分类是固定流程分类后触发什么动作是条件分支这正是 Agent 擅长的场景。我选 AgentCore 的原因有两个一是它和现有的 API 调用方式兼容迁移成本低二是它的工具注册机制比较直观不需要写太多胶水代码。3.2 Agent 和普通 API 调用的本质区别这里插一句概念澄清因为很多人把这两个搞混。普通 API 调用是“你问它答”Agent 是“你给目标它自己想办法”。打个比方API 调用像是你去餐厅点菜你说“来份宫保鸡丁”厨房做好端给你。Agent 像是你告诉服务员“我想吃点辣的、有花生的、下饭的”服务员去跟厨房沟通可能推荐宫保鸡丁也可能推荐辣子鸡丁甚至可能先问你一句“花生过敏吗”。落到代码上API 调用是你控制流程Agent 是模型控制流程。这个区别决定了API 调用的输出是可预测的Agent 的输出需要做更多的容错处理。3.3 工具注册与编排的实操细节AgentCore 里每个能力都要注册成一个工具。我注册了三个工具查订单、查物流、查账号状态。from agentcore import Agent, tool tool def query_order(order_id: str) - dict: 根据订单号查询订单状态 # 实际调用内部订单系统 return {order_id: order_id, status: 已发货, amount: 299.00} tool def query_logistics(order_id: str) - dict: 根据订单号查询物流轨迹 return {order_id: order_id, latest: 已到达杭州转运中心} tool def query_account(user_id: str) - dict: 查询账号状态 return {user_id: user_id, status: 正常, frozen: False} agent Agent( modelgpt-4o, tools[query_order, query_logistics, query_account], system_prompt你是客服工单处理助手。先分类工单再根据类别调用相应工具获取信息最后给出处理建议。 )工具函数的 docstring 非常关键。Agent 是靠这段描述来判断什么时候该调用哪个工具的。我一开始写得很随意结果 Agent 经常调错工具。后来把 docstring 写清楚明确说明“这个工具在什么场景下使用”准确率明显提升。3.4 踩坑记录Agent 的“自作主张”与安全边界坑一Agent 会调用不该调用的工具。有次一个产品咨询类工单Agent 莫名其妙去查了订单。原因是工单里提到了“我上次买的那个”触发了订单查询的关键词匹配。解决办法是在 system prompt 里加约束“只有在工单明确涉及订单问题时才调用订单查询工具。”坑二工具调用失败后的处理。订单系统偶尔会超时Agent 拿到空结果后不知道怎么办会反复重试。我在工具函数里加了异常捕获失败时返回一个明确的错误信息让 Agent 知道“这条路走不通换一条”。坑三Agent 的响应时间不可控。一次工单处理可能涉及 2-3 次工具调用每次都要等模型决策总耗时比直接 API 调用长不少。对于实时性要求高的场景这个延迟需要提前评估。我的做法是给 Agent 设置最大工具调用次数超过就强制返回当前结果。4. 第三版做成 Claude Code 插件把能力嵌进工作流4.1 为什么要把 Agent 做成插件第二版上线后工程师团队提了个需求他们平时在 Claude Code 里写代码遇到工单相关的 bug 时希望能直接在编辑器里查工单分类结果不用切到客服系统。这就引出了第三版——把分类能力做成 Claude Code 插件。插件形态的价值在于场景嵌入。API 和 Agent 都是独立服务用户需要主动去调用。插件是嵌在用户已有的工作流里的用户不需要改变习惯能力就自然触达了。4.2 Claude Code 插件的基本结构Claude Code 的插件机制基于 MCPModel Context Protocol核心是提供一个工具服务让 Claude Code 能调用你的能力。插件目录结构大致如下ticket-classifier-plugin/ ├── manifest.json ├── server.py └── requirements.txtmanifest.json定义插件的基本信息和工具列表{ name: ticket-classifier, version: 1.0.0, description: 客服工单分类与查询工具, tools: [ { name: classify_ticket, description: 对工单文本进行分类, parameters: { type: object, properties: { text: {type: string, description: 工单内容} }, required: [text] } } ] }server.py实现具体的工具逻辑本质上就是把第二版的 Agent 能力包装成一个 MCP 服务from mcp.server import Server from mcp.types import Tool, TextContent server Server(ticket-classifier) server.list_tools() async def list_tools(): return [Tool( nameclassify_ticket, description对工单文本进行分类, inputSchema{...} )] server.call_tool() async def call_tool(name, arguments): if name classify_ticket: result classify_ticket(arguments[text]) return [TextContent(typetext, textresult)]4.3 插件开发中的关键决策决策一插件里放多少能力。我一开始想把所有功能都塞进插件后来发现插件工具太多会让 Claude Code 的选择变困难。最后只保留了最核心的“分类”和“查询”两个工具其他能力还是走独立服务。决策二本地执行还是远程调用。插件可以本地跑逻辑也可以调用远程 API。我选了远程调用因为分类模型和业务数据都在服务端本地跑不现实。代价是插件依赖网络离线环境下不可用。决策三错误处理策略。插件调用失败时是返回错误信息还是静默失败我选择返回明确的错误信息让 Claude Code 知道发生了什么这样用户能看到“工单服务暂时不可用”而不是莫名其妙没反应。4.4 踩坑记录插件调试的痛点坑一插件加载失败没有明确提示。Claude Code 加载插件失败时报错信息往往很模糊。我的排查方法是先单独跑server.py确认 MCP 服务本身能启动再检查 manifest 配置。分步排查比一次性调试快得多。坑二工具描述写得太技术化。一开始我把工具描述写成“调用分类模型对输入文本进行多类别分类”结果 Claude Code 很少主动调用。改成“帮我判断这个工单属于哪一类”之后调用频率明显上升。工具描述要站在使用者的角度写不是站在开发者的角度。坑三版本更新后插件失效。Claude Code 升级后MCP 协议的某些字段变了插件直接不工作。教训是插件开发要关注协议版本在 manifest 里明确声明兼容的版本范围。5. 三种方案的横向对比与选型建议5.1 核心维度对比维度直接调 APIAgentCoreClaude Code 插件开发成本低半天搞定中2-3 天中高3-5 天适用场景单轮输入输出多步骤、多工具编排嵌入已有工作流调试难度低中高响应延迟低中高取决于远程服务扩展性差好中用户触达需主动调用需主动调用场景内自然触达运维复杂度低中高5.2 我的选型判断流程每次遇到新需求我会按这个顺序问自己几个问题任务是不是一次输入一次输出是的话直接调 API别犹豫。需不需要根据中间结果决定下一步需要的话上 Agent 框架。用户是不是已经在某个工具里工作了是的话考虑做成插件嵌入。团队有没有维护服务的能力没有的话优先选最简单的方案。这个流程帮我避免了很多过度设计。我见过太多项目明明一个 API 调用能解决非要搭一套 Agent 架构最后维护成本高得离谱。5.3 一个容易被忽略的成本模型调用的费用三次实现里模型调用费用差异很大。直接调 API 最省因为只调一次。Agent 因为要多轮决策费用可能是 API 的 3-5 倍。插件本身不增加模型调用但如果插件触发了 Claude Code 的额外推理费用也会上升。我做过一个粗略统计同样处理 1000 个工单直接 API 调用花费约 2 元Agent 方案约 8 元插件方案约 10 元含 Claude Code 侧的消耗。量小的时候无所谓量大了这个差异很可观。6. 常见问题速查与避坑清单6.1 认证与配置类问题问题现象可能原因解决方法401 unauthorizedkey 含空格或换行调用前 strip() 处理401 unauthorizedkey 过期或权限不足检查 key 有效期和权限范围400 context length输入超过模型上限截断输入或分批处理连接超时网络问题或服务端限流加重试机制设置合理超时6.2 Agent 行为类问题问题现象可能原因解决方法调用错误的工具工具描述不清晰重写 docstring明确使用场景反复重试失败工具工具返回信息不明确失败时返回明确错误信息响应时间过长工具调用轮次过多设置最大调用次数限制输出格式不稳定prompt 约束不足明确输出格式要求6.3 插件开发类问题问题现象可能原因解决方法插件加载失败manifest 配置错误单独测试 server 再查配置工具不被调用描述太技术化用自然语言重写描述升级后失效协议版本不兼容声明兼容版本范围调用无响应远程服务不可用加超时和错误提示6.4 几条用血泪换来的经验经验一先用最笨的办法跑通再考虑优化。我第一版 API 调用虽然简陋但它让我快速验证了需求可行性。如果一上来就搭 Agent可能花了一周还在调框架需求本身反而没验证。经验二prompt 的稳定性比模型能力更重要。同一个模型prompt 写得好和写得差效果差距可能比换模型还大。花时间打磨 prompt比花时间比较模型性价比高。经验三给所有外部调用加超时和重试。不管是 API 调用、工具执行还是插件通信网络问题永远存在。没有超时机制的代码在生产环境就是定时炸弹。经验四日志要记全但别记敏感信息。工单内容可能包含用户隐私日志里要脱敏。但请求 ID、耗时、错误码这些要记全排查问题时全靠它们。经验五别追求一次做对留好回滚路径。三次实现我都是新开分支旧版本继续跑着。新版本验证没问题再切换出问题能快速回退。这个习惯救过我好几次。7. 后续可以怎么扩展这套东西跑到现在我又在琢磨几个方向。一是把分类模型换成更小的本地模型降低调用成本和延迟二是把插件能力扩展到其他编辑器让更多同事能用上三是给 Agent 加上缓存层相同工单不重复处理。如果你也在做类似的东西我的建议是别被“Agent”这个词吓到。它本质上就是个能自己决定下一步做什么的程序核心还是把需求拆清楚、把边界定明白。工具选型没有绝对的对错只有适不适合当前场景。先用最简单的方式跑通遇到瓶颈再升级这个节奏比什么都重要。
返回列表