☰
从API到Agent再到插件:客服工单分类Agent的三次造轮子实践
2026/10/4 9:50:54 网站建设 项目流程

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": f"Bearer {API_KEY}", "Content-Type": "application/json" } resp = requests.post(API_URL, json=payload, headers=headers, timeout=30) 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 model's maximum context length is 1048576 tokens的报错。虽然这个上限看着很大,但实际业务里确实会遇到超长输入。我的处理方式是在调用前做一次截断,超过 8000 字符的部分直接砍掉,因为工单的核心诉求通常在开头。

坑三:并发上来之后接口开始超时。客服高峰期同时有几十个工单进来,同步请求直接排队。后来改成了批量处理,一次请求塞多个工单,让模型返回 JSON 数组。这里要注意,批量处理时 prompt 的结构要更严格,否则模型容易把多个工单的结果混在一起。

def classify_batch(tickets): numbered = "\n".join([f"{i+1}. {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( model="gpt-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 的插件机制基于 MCP(Model Context Protocol),核心是提供一个工具服务,让 Claude Code 能调用你的能力。插件目录结构大致如下:

ticket-classifier-plugin/ ├── manifest.json ├── server.py └── requirements.txt

manifest.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( name="classify_ticket", description="对工单文本进行分类", inputSchema={...} )] @server.call_tool() async def call_tool(name, arguments): if name == "classify_ticket": result = classify_ticket(arguments["text"]) return [TextContent(type="text", text=result)]

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 我的选型判断流程

每次遇到新需求,我会按这个顺序问自己几个问题:

  1. 任务是不是一次输入一次输出?是的话,直接调 API,别犹豫。
  2. 需不需要根据中间结果决定下一步?需要的话,上 Agent 框架。
  3. 用户是不是已经在某个工具里工作了?是的话,考虑做成插件嵌入。
  4. 团队有没有维护服务的能力?没有的话,优先选最简单的方案。

这个流程帮我避免了很多过度设计。我见过太多项目,明明一个 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”这个词吓到。它本质上就是个能自己决定下一步做什么的程序,核心还是把需求拆清楚、把边界定明白。工具选型没有绝对的对错,只有适不适合当前场景。先用最简单的方式跑通,遇到瓶颈再升级,这个节奏比什么都重要。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询