年初我把公司那套用了两年的客服机器人拆了重做,内部代号就叫Agent-Reach。拆之前,它只能算“语音应答脚本+关键词匹配”,用户问一句它答一句,稍微绕一点就答非所问,更别提让它帮忙建工单、查库存、改预约,完全没有这个能力。真正触发我重写的原因是去年底的运营数据:87%的会话在首轮结束后就沉默,线索转成交率不到4%。换句话说,机器人大部分时间在讲废话,没有把“触达流量”变成“可跟进业务”。
Agent-Reach 说白了就是用大模型底座搭建的一套“能办成事的智能体系统”,核心不是让它更会聊天,而是让它像一个人一样,把用户的意图拆解成动作,再通过后端的工具、API、人工协作真正把事情推进下去。客户说“我想约个明天下午的演示”,它不只是回复“好的,已记录”,而是真的把时间写进 CRM、发日历邀请、同步给销售同事。这套系统适合谁?适合手里有私域流量、却一直卡在“机器人只会聊不会干”阶段的团队,也适合所有想从“客服机器人”往“自动化业务员工”迈一步的开发者。
这篇文章我会把 Agent-Reach 从需求拆解、架构设计、模块实现到上线踩坑,全链路讲一遍。中间会给出配置文件、核心代码段、阈值参数和排查表格,尽量做到看完就能在你的系统里复制一套。
1. 项目定位:为什么说“会聊天”和“能办事”是两套完全不同的系统
1.1 用户要的不是聪明,而是“办成”
做智能体之前,我习惯先问一个问题:用户走到对话窗口前,他到底想要什么?大部分情况不是想跟 AI 唠嗑,而是想解决一件具体的事。
比如用户说“你们那个套餐还有没有货”。过去机器人要做的是:查知识库,找到“套餐说明”,然后把标准答案发过去。用户如果追问“多少钱”“怎么买”,机器人再查一遍。如果用户说“我想退了重订”,完蛋,知识库里没有现成答案,机器人只会说“抱歉,我暂时无法处理,请转人工”。整条链路里,机器人是个“信息检索器”,而不是“业务执行者”。
Agent-Reach 把逻辑倒过来。每一个会话进来,先判断用户意图:是咨询、下单、退订、改约,还是找人工。每个意图背后挂的不是一段回答,而是一个“动作”。动作可以调库存系统,可以在订单系统里建单,可以创建售后工单,也可以直接把会话转给指定队列的人工客服。关键变化在于:对话只是入口,动作才是结果。
这也是我把它命名为“Reach”的原因——触达的终点不是回复框里的那行字,而是业务系统里真实发生的一条记录。智能体有没有价值,不看它接住了多少句用户提问,而是看它替业务团队完成了多少次可追踪的执行。
1.2 从“答题机器人”到“智能体”必须补的三块骨架
把普通机器人升级成智能体,不是换个大模型就完事。我拆了三块骨架:
第一块是状态管理。普通机器人每次提问都是独立的,用户上一句说“我要买个贵的”,下一句说“算了还是便宜的”,机器人如果记不住上下文,就会推荐错。智能体需要维护一个会话状态机,记录用户是谁、到哪一步了、已经填了哪些字段。
第二块是动作协议。不是所有能力都塞给大模型让它自由发挥。Agent-Reach 里每个可执行动作都定义成一份 JSON Schema,包括参数、类型、必填项、副作用。大模型负责从自然语言里抽取参数,抽取完之后按 schema 校验,校验通过才允许执行。这一步非常重要,没有协议约束,模型经常“自由发挥”出一堆系统里根本不存在的字段。
第三块是兜底与人工交接。智能体一定会遇到处理不了的情况,比如用户发了一段非常复杂的售后描述、情绪激动、或者一句话里包含三个意图。与其硬答,不如果断转人工。Agent-Reach 在架构上从一开始就设计了人工前台——智能体先把上下文摘要推给人工客服,减少人工重复提问。
这三块骨架缺一块,系统就还是“语音应答脚本的现代版”。
1.3 技术选型背后的取舍逻辑
技术栈按“稳定优先、团队能维护”的原则选,不是追最新:
- 语言与 Web 框架:Python 3.11 + FastAPI。团队本来就会 Python,FastAPI 自带 OpenAPI 文档,后面做工具接口暴露非常方便。
- 任务队列:Celery + Redis。动作执行、异步通知、定时跟进都用 Celery。Redis 同时承担会话状态缓存的职责,双复用。
- 主存储:PostgreSQL。会话记录、动作日志、账单流水全是结构化数据,放 PG 最稳。
- 向量存储:pgvector。历史相似问题检索不需要单独引入 Elasticsearch,pgvector 在 PG 内部解决,减少一套运维。
- 大模型层:统一走一个 Model Gateway,里面可以切不同厂家的模型。不推荐业务代码直接写死某个模型 API,因为大模型迭代太快,今天用的模型三个月后可能效果更好的替代品已经出来了。
这套组合的好处是:每一层都有成熟生态,出问题时社区资料多,招人培训成本低。坏处是:没有用最潮的框架,但做商业系统,稳定性永远优先于炫技。
2. 核心模块设计与实现:五大模块把“触达到执行”串成一条线
2.1 渠道接入层:每一路渠道都做“适配器”,前台统一收口
Agent-Reach 要接的渠道不止一个:企业微信、微信公众号、网页悬浮窗、小程序客服,甚至未来的钉钉、飞书。不同渠道的 API 形态完全不一样,推送消息的方式也不同,如果每个渠道单独写一套对话逻辑,代码量会爆炸。
我的做法是所有渠道都抽象成“适配器”。适配器只负责三件事:
- 接收渠道推送的事件(新消息、用户进入、点击菜单);
- 把消息内容转换成系统内部统一的
InboundMessage结构; - 接收智能体产生的回复,通过渠道 API 发出去。
内部统一消息结构大概是这样的(简化版):
@dataclass class InboundMessage: channel_type: str # wecom / mp / web / mini_app channel_user_id: str # 各渠道的用户ID session_id: str # 渠道侧会话ID,适配器标准化成统一格式 msg_type: str # text / image / voice / event content: str # 文本内容,如果是语音则走ASR转写后填入 raw: dict # 原始报文,排查问题时会用到为什么要做这层标准化?因为后端的意图识别、会话管理、动作编排,根本不需要关心用户是从哪儿来的。开发新渠道时,只需要写一个适配器,后台逻辑一行不用改。目前 Agent-Reach 已经接了三路渠道,新增小程序渠道时只花了一个下午,收益非常明显。
渠道接入时会遇到一个常见的坑:回调地址与验签。企业微信、公众号都会要求配置回调 URL,并且会带签名参数。不少人图省事直接跳过验签,结果就是来源不可信,别人可以伪造消息。我建议验签逻辑放到适配器最外层,验不过直接丢弃,不进入后续任何流程。
2.2 会话上下文层:用“结构化状态 + 语义记忆”两条线防止失忆
用户和智能体说话,天然默认对方记得自己刚才说了什么。但大模型的上下文窗口是有限的,用户聊二十分钟以后,早期信息很容易被挤掉,或者被模型误解。Agent-Reach 把上下文拆成两条线:结构化状态和语义记忆。
结构化状态存的是业务关键字段。比如用户预约演示的流程,系统里会维护一个AppointmentStateDict:
{ "user_id": "u_10086", "current_step": "collect_time", "intent": "book_demo", "slots": { "company_name": "某某科技", "contact_name": "张三", "contact_phone": "138****8888", "expected_date": "2026-05-20", "expected_time": "14:00" }, "confirmed": false }这个状态存在 Redis 里,key 是session:state:{session_id},超时时间设为 30 分钟。用户每说一句话,都会先把这句话拿去更新 slot,再决定下一步回复什么。也就是说,模型不需要从整段对话里“猜”用户已经填了什么,直接从 dict 里读就行,准确率高得多。
语义记忆是另一条线。当用户用自然语言描述过一些偏好,比如“我预算大概在三万左右”“我们团队比较看重报表功能”,这些信息不属于任何业务字段,但会影响后续推荐。Agent-Reach 会把这类信息向量化存到 pgvector,标记为preference类型,在后续生成回复时作为附加记忆一起送入模型。
有一个实操中的性能教训:不要每次回复都把全部历史消息塞给模型。一开始我图省事,直接把最近 20 轮消息全部拼进 prompt,结果 token 消耗大、响应延迟高,而且后半段的上下文明显“污染”,模型经常用无关信息。后来改成:最近 3 轮消息完整文本 + 当前结构化状态 + 语义记忆里最相关的 5 条,效果明显好转。
2.3 意图识别与动作编排层:让大模型学会“先想再做”
意图识别是整套系统的核心。Agent-Reach 采用“分类器初筛 + 大模型兜底”的两级方案,而不是所有判断都交给大模型。
第一级是轻量分类器。我们在线上积累了历史会话数据后,整理出 12 个高频意图:book_demo、check_price、check_stock、create_order、cancel_order、create_ticket、change_appointment、ask_human、greeting、thanks、complain、other。用 FastText 或 Sentence-BERT 这类模型先做一轮初筛,速度极快,推理开销极低,常规意图在几十毫秒内就能出来。
第一级如果置信度超过 0.92,直接进入对应的动作流程,不走大模型,既省成本又稳定。如果置信度在 0.6-0.92 之间,进入第二级:把用户原话、结构化状态、意图候选列表打包成 prompt,让大模型做最终判断并抽取参数。置信度低于 0.6 时,直接转人工兜底。
动作编排层是一个轻量的“工具注册表”。每个工具就是一个函数 + 一份 schema:
@agent_tool.register( name="create_order", description="根据用户选定的套餐创建订单", parameters={ "type": "object", "properties": { "sku_id": {"type": "string", "description": "套餐SKU ID"}, "quantity": {"type": "integer", "minimum": 1, "default": 1}, "customer_phone": {"type": "string", "description": "客户手机号"} }, "required": ["sku_id", "customer_phone"] } ) def create_order(sku_id: str, quantity: int, customer_phone: str) -> dict: # 调用订单系统API,创建订单,落库 pass大模型从用户的自然语言里抽取参数,然后工具注册表按 schema 校验。校验不通过就反问用户补齐缺失字段。校验通过才真正执行。
这里有一条重要原则:所有工具执行必须幂等。智能体调用后端系统时,网络超时会重试,重试可能造成重复下单。所以每次执行动作前生成一个action_id,后端系统用这个 ID 做去重,同一个 action_id 只允许成功执行一次。
2.4 人工兜底层:智能体要懂得“什么时候该认怂”
再聪明的智能体也有处理不了的时候。Agent-Reach 把“转人工”从异常分支提升为一种正常流程,反而让系统整体更可靠。触发转人工的条件有五类:
- 用户明确说“我要找真人”“人工客服”“你让活的来”;
- 意图识别置信度低于阈值,且大模型也无法判断;
- 情绪负面判断:文本分类判断用户处于愤怒、失望状态;
- 连续三轮没有推进任何状态(机器人反复回答但用户不满意);
- 高风险动作:退款金额超过阈值、投诉工单、涉及合同条款确认。
转人工不是简单地把会话丢给某个客服。Agent-Reach 会生成一份“会话交接摘要”,包含三块:用户已经做了什么、用户目前想做什么、系统建议人工做什么。人工客服接入后,第一屏看到的就是这份摘要,不需要从聊天记录里翻。
注意:很多团队不愿意设置“低置信度转人工”,觉得会显得机器人“笨”。实际上,判断失误继续硬聊,用户流失率远高于早转人工。让真人从比较有价值的节点接手,整体转化率反而是提升的。
2.5 数据回看层:会话不只是“聊过”,而是要长出复利
上线第一周我就发现,如果没有数据回看能力,整个系统的迭代就是盲人摸象。Agent-Reach 的每个动作都会记录日志:
event_id:唯一事件 ID;session_id:会话 ID;user_id:用户 ID;channel_type:渠道;intent:识别出的意图;tool_name:执行的动作名;tool_params:动作参数;tool_result:动作返回结果;latency_ms:单次动作时延;success:是否成功;error_msg:失败时的错误信息。
有了这些日志,可以跑转化漏斗分析:某渠道来了多少用户、多少走到意图识别、多少完成动作、多少转人工、最终成单多少。我每周都会看这个漏斗,重点盯“机器人会话到工具执行”的转化率,只要这个指标连续两周下滑,一定是有意图分错或者动作链路故障,及时排查。
另外,日志也用来做持续数据飞轮。每周从日志里抽“失败案例”和“人工接管案例”,让运营人员标注正确结论,重新训练意图分类器。这是智能体系统摆脱“上线即巅峰”的关键一步。
3. 实操部署与冷启动:七个步骤把系统从代码变成生产服务
3.1 环境准备与依赖选型
先交代我的服务器配置参考:2 核 4G 起步,建议 4 核 8G。大模型调用走外部 API,本地不跑推理,所以 CPU 资源主要被 FastAPI 和 Celery 占着。如果未来要在本地跑小模型做意图识别,再加一张推理卡即可。
依赖清单大致如下:
# Python 3.11 fastapi uvicorn[standard] celery[redis] redis sqlalchemy psycopg2-binary pgvector pydantic sentence-transformers # 用于意图初筛 openai # 或其他模型网关SDK python-dotenv部署顺序:先把 PostgreSQL 和 Redis 起起来,PostgreSQL 安装 pgvector 扩展,然后建库建表。建议用 Docker Compose 管理中间件:
version: "3.9" services: postgres: image: pgvector/pgvector:pg16 environment: POSTGRES_USER: agent_reach POSTGRES_PASSWORD: xxxxx POSTGRES_DB: agent_reach ports: - "5432:5432" volumes: - pg_data:/var/lib/postgresql/data redis: image: redis:7-alpine ports: - "6379:6379" volumes: pg_data:3.2 接入第一个渠道:以企业微信私域场景为例
企业微信是目前私域运营最常见的载体,接入过程也最有代表性。第一步是在企业微信管理后台创建“客户联系”应用,拿到corpid、corpsecret,配置回调 URL。
回调 URL 需要公网可达,并且要填 Token 和 EncodingAESKey。Agent-Reach 的适配器里,验签与解密逻辑用官方的wechatpy库封装即可,注意一个坑:回调 URL 必须返回success字符串,否则企业微信会认为失败并连续重试。
接入后的第一件事,不是写业务逻辑,而是先做一个“回声测试”:让用户在企微里发任何消息,机器人原样回复。这个测试能确认通道是通的,再往上叠业务逻辑。如果用真实业务直接压测,出了问题你会分不清是通道问题还是逻辑问题。
3.3 定义第一个业务动作:以“预约演示”为例
任何团队第一次做智能体,我都不建议一上来接订单退款这种高风险动作。先从“预约演示”这种低风险、信息标准化程度高的动作起步。
预约动作的核心函数:
@agent_tool.register( name="book_demo", description="为用户预约产品演示", parameters={ "type": "object", "properties": { "company_name": {"type": "string", "description": "公司名称"}, "contact_name": {"type": "string", "description": "联系人姓名"}, "contact_phone": {"type": "string", "description": "手机号"}, "expect_date": {"type": "string", "description": "期望日期,格式YYYY-MM-DD"}, "expect_time": {"type": "string", "description": "期望时间,格式HH:MM"} }, "required": ["contact_name", "contact_phone", "expect_date"] } ) def book_demo(company_name, contact_name, contact_phone, expect_date, expect_time): # 校验时间是否已被占用 # 写入预约表 # 调用calendar API创建日程 # 返回预约ID pass重点讲一下“必填字段”的设计。我观察过真实对话,用户通常第一句只会说“我要预约演示”,后面跟着公司名或姓名。如果把所有字段都设为必填,模型会一口气反问五个问题,体验极差。更合理的做法是:首轮只要求contact_name和contact_phone必填,company_name可以留空,expect_date如果没说明确,默认推最近三个可选时间让用户挑。
对话流程如图(文字示意):
用户:我想约个演示
智能体:请问怎么称呼您?方便留一个手机号吗?
用户:我叫李四,电话 13800001111。
智能体:好的李四,最近可约的时段有明天上午10点、明天下午2点、后天上午11点,您方便哪个?(如果你的时间更具体,也可以直接告诉我)
用户:明天下午两点吧。
智能体:已为您预约明天14:00的演示,稍后会把日历邀请发给您。
这段对话里,智能体做了“槽位补充”“选项引导”“确认”三件事,全程没有把控制权交给用户随便乱说,这就是动作编排带来的确定性。没有动作编排的智能体会怎么回?大模型可能会直接编一个时间出来,或者回复“好的已记录”,但其实什么都没发生。
3.4 冷启动话术模板与阈值参数设计
冷启动阶段最大的问题是“没有历史数据”,模型不知道什么话术效果好。Agent-Reach 预置了几套基于最大共识的模板,你可以先用:
第一,澄清话术。模型抽取参数不全时,不要直接说“参数缺失”,要说:“为了帮您落实,还需要确认一下……”
第二,时间引导话术。用户没说具体时间时,永远不要问“您什么时候方便?”这种开放问题,给选项:“本周三上午10点、本周四下午2点、本周五上午11点,您看哪个合适?”
第三,转人工话术。不要说“我无法处理”,要说:“这个问题我需要请同事帮您确认,正在为您转接人工,请稍等。”
阈值参数同样需要冷启动校准。我的初始配置供参考:
| 参数 | 初始值 | 说明 |
|---|---|---|
| 意图初筛置信度阈值 | 0.60 | 低于此值不进入大模型二次判断,直接转人工 |
| 大模型直接执行阈值 | 0.92 | 高于此值直接执行动作 |
| 整体回复最大延迟 | 3500ms | 超过不算成功,计入告警 |
| 单动作调用超时 | 10s | 超过则按失败处理,允许重试一次 |
| 上下文保留最近轮数 | 3 | 完整原文保留3轮 |
这些参数上线后再用真实数据调整。比如转人工率太高,就把 0.60 调到 0.55;机器人乱执行动作,就把 0.92 提高到 0.95。
3.5 灰度发布与 AB 实验
智能体系统最怕“一步到位”。我的上线策略是:先 5% 流量跑三天,再 30% 跑三天,最后全量。灰度期间重点盯四个指标:
- 转人工率:正常应该在 15%-30%。太低说明机器人可能在硬答,太高说明系统兜不住;
- 动作执行成功率:低于 85% 必须回滚;
- 平均响应时延:超过 4 秒用户会明显流失;
- 用户负面反馈率:可以做简单的文本情感判断,也可以抽样人工听。
AB 实验方面,我会把话术模板做成配置化的。同一个意图下,可以配置两套不同话术,按 session_id 哈希分流。跑一周之后看哪个话术的动作完成率高,再全量切换。不要用“感觉”,用数据说话。
4. 实战中踩过的坑与排查方法:跑了一百天,最头疼的是这五个问题
4.1 意图误判:同义词与“说一半”问题
上线第一周就遇到一个典型案例:用户问“你们有没有便宜的套餐”,模型意图识别成了check_stock,但用户实际意图是check_price。这两个意图在语义上非常接近,但动作完全不同——查库存和查价格调用的是两个接口。
排查时发现,历史训练数据里对于“便宜”、“多少钱”、“价位”这类词标注不够。解决办法很朴素:人工补充 200 条带“价格意图”标签的样本,重新微调意图分类器,同时调整 prompt,让大模型在抽参时额外输出 intent_confidence。
另外一个高频坑是“说一半”。用户说“我想预约”,会话状态停在collect_time,然后用户又发来一句“对了你们支持私有化部署吗?”——这句话完全不属于预约流程。Agent-Reach 的处理是:当新消息与当前状态不匹配时,先并行判定两个意图:一个是“继续当前流程”,一个是“发起新话题”。如果是新话题,先回复简短回答,再拉回原流程,不要直接打断当前预约流程。
4.2 上下文污染:会话窗口过长导致幻觉
很多人以为上下文越长信息越全,我在 Agent-Reach 里实测下来完全不是。有一段时间用户经常投诉:机器人把 A 公司说成 B 公司,把“不要短信”记成“要短信”。排查后发现是 prompt 里的历史消息太长,模型把早期某句话误解成了最新要求。
我给出的解决方案就是前面讲到的“结构优先”记忆策略。把业务状态抽离出来,模型只看到当前关键信息,不看到完整聊天记录。比如预约流程中,模型只看到:
当前流程:book_demo 已完成字段:company_name=某某科技, contact_name=张三 待补字段:contact_phone, expect_date 最近用户消息:手机号是13800001111这样模型每次只需要处理“新增信息”,不需要从一大坨历史里找答案,幻觉率肉眼可见地下降。实践下来,约束越多,模型越稳。
4.3 幂等与重试:扣了钱但订单没创建
这是整套系统最严重的一次事故。用户要下单,智能体调用了订单创建接口,接口返回超时,系统触发重试。结果实际上下单接口第一次就成功了,重试又创建了第二个订单,用户被扣了两笔钱。
根治办法就是前面提到的action_id幂等键。每次动作生成唯一的action_id,带着一起去调后端接口,后端收到后先去幂等表查,查到了直接返回第一次的结果。
def create_order_with_idempotency(action_id, payload): existing = get_idempotency_record(action_id) if existing: return existing.result try: result = call_order_api(payload) save_idempotency_record(action_id, result) return result except TimeoutError: # 不立即报错,查询后端是否已生成订单记录 order = query_order_by_action_id(action_id) if order: save_idempotency_record(action_id, order) return order raise这个坑不是智能体独有的,全部自动化系统都会遇到。关键是要提前设计好,等出事了再补会非常痛苦。
4.4 延迟治理:响应慢不等于卡死
有段时间测出来平均响应延迟 6 秒,用户流失严重。拆开链路发现有四个耗时点:意图初筛 30ms、大模型调用 2.8s、动作执行 1.2s、渠道发送 0.5s,加起来 4.5s,高峰期更慢。
大模型调用是最大瓶颈。优化的两个思路:模型网关配了流式输出,用户先看到“正在输入”的状态,心理感知明显变好;同时引入模型路由,简单意图用小模型(如轻量级模型),复杂意图才用旗舰模型,平均耗时降到 2.1s。另一个思路是动作执行提前做优化:把查询接口做成批量预取,用户进入会话时猜几个高频动作,提前把价格、库存查询好缓存下来,真正执行时直接命中缓存。
4.5 常见问题速查表
| 现象 | 可能原因 | 排查方法 |
|---|---|---|
| 机器人答非所问 | 意图分类置信度低、上下文被污染 | 查日志里的 intent 和 confidence,抽样看 prompt 历史 |
| 用户说“约了”但系统没有记录 | 动作执行失败或校验不过 | 查 tool_result 和 error_msg,重点看必填参数 |
| 回复时延极高 | 大模型响应慢、外部接口慢 | 用链路追踪看各段耗时,瓶颈定位后再优化 |
| 转人工率突然飙升 | 意图阈值设太高或话术引导太差 | 看阈值配置是否改动,抽样听会话录音 |
| 同一条消息被重复处理 | 渠道回调重试、无去重逻辑 | 查 message_id 是否全局唯一去重 |
| 动作重复执行 | 缺少幂等键 | 检查 action_id 是否传递并落幂等表 |
5. 写在最后:Agent-Reach 不是模型问题,是体系问题
跑了一百天后,我最深的体会是:智能体这件事,模型能力只占三成,剩下七成是工程和运营。同样的底模,有人做出来是智障,有人做出来是得力助手,差别就在于你有没有把状态管理管好、把动作协议定清楚、把兜底流程跑起来。
如果让我给一个刚准备动手的团队一句建议:不要从“做一个聪明机器人”开始,要从“让机器人替用户办成第一件事”开始。挑一个最低风险的动作,比如预约、报名、问卷填写,跑通全链路,再逐步叠加复杂动作。Agent-Reach 之所以能稳定跑下来,就是因为我们每一步都先定义清楚“动作”,再讨论“话术”。
最后分享一个小技巧:把 Agent-Reach 的“会话交接摘要”用起来之后,人工客服的人均处理时长降了 40%。很多时候你以为智能体的价值就是少雇客服,其实它的价值是让真人客服只做真正需要人的事。谁能把“机器处理”和“人工处理”衔接得越顺,谁的用户体验就越好。这一点,在任何行业都成立。