把标题这句话拆开看,它其实浓缩了我去年搭企业技术支持 Agent 时的完整心路历程。基础版本把 RAG 接进去之后,问答已经能跑通,但每个用户问出来的答案高度雷同,敏感资料不敢放,多轮对话也经常断片。后来把 Token 体系接进来,Agent 才真正开始“变聪明”:它知道你是谁、能看什么、上次聊到哪,回答质量完全换了一个档位。
这篇文章我会把项目从零到一的完整过程拉一遍,重点拆解三件事:Token 体系怎么设计和排障、RAG 知识库怎么建设才能真正用起来、Agent 怎么在并发场景下扛住生产流量。适合正在做企业知识问答助手、内部技术支持机器人,或者想把自己手头“能跑的 Demo”升级成“能用的系统”的开发和运维同学参考。所有方案都来自真实落地经验,不是 PPT 架构。
1. 为什么企业技术支持场景需要 RAG + Token
1.1 技术支持场景的真实痛点
做企业技术支持 Agent,最难的不是模型选型,而是回答的“一致性”和“可用性”。真实场景里,员工或客户问得最多的是这几类问题:
- 操作类:怎么提单、怎么重置密码、某个功能按钮在哪里
- 排障类:登录报错、接口 5xx、数据对不上
- 政策类:报销额度、假期规则、审批流程
- 产品知识类:某版本支持哪些特性、某模块依赖什么服务
这些问题有一个共同特点:答案往往分散在多个系统里。产品手册在 Wiki,历史工单在客服系统,常见问题在 FAQ 页面,架构说明在内部文档库。用户问一个问题,需要翻好几个地方才能拼出完整答案。传统的搜索框和 FAQ 页面解决不了,因为语义表达太灵活了,同一件事可能有几十种问法。
另一个痛点是信息更新太快。技术支持的知识库不是静态的,每周都有新发版公告、新工单案例、新的坑。如果靠人工维护“标准答案”,维护成本高且永远滞后。RAG 的价值就在这:它把检索和生成解耦,文档更新后重新切块索引,模型不需要重新训练就能回答新问题。
但只做“检索 + 生成”还不够。技术支持的对话高度依赖上下文和身份。同一个问题,“普通员工”和“IT 管理员”应该得到不同的答案;同一个工单,“提单人”和“审批人”能查看的信息也不同。如果没有身份层,Agent 就只能做一个“没有记忆、没有边界”的百科全书,这在企业内网是不可接受的。
1.2 “没 Token 能用”和“有 Token 更聪明”到底指什么
标题这句话,其实触及了企业级 Agent 的两个层级。
“没 Token 能用”指的是最基础的 RAG 问答链路:用户发起提问 → 检索知识库 → 拼接 Prompt → 模型生成。这个链路确实能跑通,我一开始就是先做的这个版本。问题在于,它把每个请求都当成孤立的匿名请求,用户身份、历史会话、权限范围全部丢失。
“有 Token 更聪明”则是把身份令牌(Token)接入到 Agent 的每个环节里,让 Agent 具备四个能力:
- 身份识别:知道提问者是内部员工还是外部客户,属于哪个部门,什么角色
- 权限过滤:检索结果在进入 Prompt 之前,先按用户的权限范围过滤,没有权限的文档直接不召回
- 上下文连续:通过 Token 关联会话 ID,把多轮对话、历史工单、上一次的回答都带进当前请求
- 审计与用量:每一次提问都对应到具体用户,可以统计 Token 用量、做限流、留审计日志
需要说明的是,里外里其实有“两个 Token”需要区分。一个是身份令牌(JWT 这类鉴权凭证),另一个是大模型调用的 Token 用量。标题里“没 Token 能用”的 Token 主要指身份令牌,但“有 Token 更聪明”里的 Token 也可以理解成大模型上下文窗口的合理使用——把身份信息、权限信息、历史会话都打包进 Prompt,模型自然能给出更贴合场景的回答。
企业技术支持 Agent 的关键,就是把这两个 Token 体系打通,让鉴权身份直接决定上下文范围。
2. Token 体系设计与实现:从登录到续签
2.1 为什么选 JWT 而不是 Session
这个项目最开始有同事建议用 Session 方案,理由是简单。我的看法是:Session 在单体应用里确实简单,但技术支持 Agent 注定要独立部署、水平扩展、被多个客户端(Web 管理后台、IM 机器人、工单系统)同时调用,这时候 Session 的劣势会很突出。
我把两个方案的对比整理成一张表:
| 对比项 | Session | JWT |
|---|---|---|
| 状态存储 | 服务端内存或 Redis | 客户端持有,服务端无状态 |
| 横向扩容 | 需要集中式 Session 存储 | 任何实例都能直接校验 |
| 多客户端复用 | 需要共享 Session 存储 | Token 天然自包含 |
| 过期处理 | 服务端统一清理 | 依赖 exp 声明和刷新机制 |
| 权限变更 | 可实时失效 | 需配合黑名单或版本号 |
最终选了 JWT,核心原因是无状态。Agent 服务可以随便扩缩容,不需要引入额外的 Session 同步机制。JWT 本身包含用户 ID、角色、租户、过期时间,网关解析后就能拿到足够信息,不用每次都查数据库。
要提醒的是,JWT 选型时的一个大坑是密钥管理。我在项目里专门搞了一个密钥管理方案,定期轮换 JWT 签名密钥,同时在网关层缓存了公钥,避免每次请求都去认证中心拉密钥导致性能下降。
2.2 access token + refresh token 双令牌方案
光有 JWT 还不够,生产环境必须用双令牌:短期 access token + 长期 refresh token。为什么?如果只有一个长期 Token,一旦泄露,攻击者可以用很久;如果只有一个短期 Token,用户体验极差,每十分钟就要重新登录一次。
我采用的是标准双令牌流程:
- 用户登录成功后,认证中心签发一对 Token:access token(有效期 15 分钟)和 refresh token(有效期 7 天)
- 客户端请求时在 Authorization 头携带 access token
- Agent 网关校验 access token 签名和过期时间,通过后放行
- access token 过期后,网关返回 401,客户端收到后用 refresh token 调用刷新接口
- 刷新接口校验 refresh token 合法后,签发新的 access token,同时轮换 refresh token(返回新的 refresh token,旧的立即失效)
这里有几个容易被忽略的细节:
- refresh token 一定要做轮换,否则旧 token 可以无限期续命
- 用户登出或修改密码后,要把对应的 refresh token 加入黑名单或做版本号自增
- 刷新接口要做设备绑定,同一个令牌不能同时在两个设备上使用(具体看业务,安全性要求高可以做)
这套流程跑通之后,用户基本感受不到 Token 过期,Agent 还能拿到最新的用户权限信息(比如员工角色刚变了,下一次刷新后立刻生效)。
2.3 Token 过期、续签和常见参数配置
JWT 里最核心的三个时间参数是:
iat(签发时间)exp(过期时间)nbf(生效时间)
我们项目里踩过一个坑:网关服务器时间比认证中心快了半分钟,导致刚签发的 Token 被判定为过期。后来统一接了 NTP 时钟同步,并在校验时加了 30 秒的时钟偏移容忍,问题才解决。
另一个经验是不要只看exp字段。Token“失效”的原因很多:
| 失效原因 | 排查方向 |
|---|---|
| 过期(exp 到达) | 走 refresh 流程 |
| 尚未生效(nbf 在未来) | 检查签发时间与服务器时钟 |
| 签名密钥轮换 | 确认网关是否缓存了旧公钥 |
| 用户被登出/权限变更 | 检查 refresh token 黑名单或版本号 |
| 租户或 scope 不匹配 | 检查 JWT claims 中的租户 ID |
为了排查方便,我每个 JWT 都带了jti(唯一 ID),并在网关日志里记录了每个请求的 jti、用户 ID、校验结果。这样用户投诉“我明明登录了怎么又失效”时,能把日志拉出来看一眼。
3. RAG 知识库建设:让 Agent 真正“有据可查”
3.1 知识底座选型:FAQ 库、文档库、结构化库、知识图谱的区别
RAG 不是银弹,也不是所有知识都适合丢进向量数据库。我在项目里花了很大精力做知识分类,总结下来四类底座各有用武之地:
| 知识类型 | 适合场景 | 实现方式 | 成本 |
|---|---|---|---|
| FAQ / 结构化问答 | 高频、答案确定、变化少 | 关键词匹配 + 规则兜底 | 低 |
| 非结构化文档 | 产品手册、公告、历史工单 | 向量检索 + 关键词检索 | 中 |
| 结构化数据库 | 订单、库存、设备配置、人员信息 | Agent 调用 SQL/API 实时查询 | 中高 |
| 知识图谱 | 实体关系复杂、需要多跳推理 | 本体建模 + 图查询 | 高 |
初期最容易犯的错误是什么都想做 RAG。我有段时间把 SQL 数据直接灌进向量库,检索效果很差,因为“订单号 WO-2025-001”这种精确值不是靠语义能搜出来的,该让 Agent 去调 HTTP API 或查数据库。后来定了原则:明确的、格式化的数据走接口查询;非结构化的文本知识才走 RAG。
知识图谱我没铺开做,但技术调研发现,对于“产品 A 依赖模块 B,模块 B 升级会影响哪些客户”这类问题,纯向量检索完全招架不住,本体约束(ontology)能显著提升召回准确性。如果团队资源够,建议用 KG 处理核心的实体关系场景。
3.2 文档切分与向量化:整本手册不能直接扔进 Embedding
做过 RAG 的人都知道,切分策略几乎决定了检索效果的 70%。我最初直接把整本产品手册当一个 chunk 去向量化,结果检索回来的内容又长又杂,模型根本没法学到重点。
我们最终沉淀出一套切分规范:
- 单块控制在 1000~2000 字左右,太短语义不完整,太长检索噪声太大
- 相邻块设 100~200 字的重叠(overlap),防止关键信息被拦腰截断
- 优先按照文档的标题层级切分,而不是按固定字数,保证每块是一个语义完整的小节
- 每块保留元数据:来源、版本号、适用产品、目标用户、更新日期
元数据特别重要。技术支持场景里经常有版本差异,同一操作步骤在 V2.1 和 V3.0 里不一样。如果不按版本过滤,检索结果会混在一起,Agent 就会一本正经地给出错误答案。我做了两件事:第一,文档入库时强制填写版本元数据;第二,检索时先从用户 Token 或对话上下文中判断当前的产品版本,作为过滤条件传入。
3.3 混合检索与重排序:为什么单用向量检索会翻车
向量检索擅长理解“意图”,但对精确值无能为力。我测试时遇到过很典型的情况:用户问“帮我查一下工单 WO-2025-001 的处理进度”,向量检索会给出一堆工单相关的通用流程文档,而不是这张工单的具体信息。因为“WO-2025-001”这个字符串在向量空间里几乎没有语义特征。
解决方案是混合检索:向量检索 + 关键词(BM25)检索双路召回,再做融合排序。我用的融合策略是 RRF(Reciprocal Rank Fusion),简单有效,不用训练模型。
下面是我验证过的基础版混合检索流程:
# 伪代码:混合检索 + RRF 重排 def hybrid_search(query_text, top_k=10): vector_hits = vector_store.search(query_text, top_k) keyword_hits = bm25_index.search(query_text, top_k) scores = {} for rank, hit in enumerate(vector_hits): scores[hit.id] = scores.get(hit.id, 0) + 1 / (60 + rank) for rank, hit in enumerate(keyword_hits): scores[hit.id] = scores.get(hit.id, 0) + 1 / (60 + rank) return sorted(scores.items(), key=lambda x: x[1], reverse=True)[:top_k]RRF 的好处是鲁棒,不依赖两路召回的顺序,但精度到不了顶尖水平。对检索排序要求更高的场景,可以直接上 cross-encoder 重排模型,效果明显更好,只是耗时和成本会涨。企业技术支持场景里,这条路值得走,因为交互式问答的检索量不大,重排的代价完全可以接受。
3.4 知识库能存图片吗:多模态资料的处理经验
很多人问“RAG 知识库能存图片吗”,真实答案是:不是把图片塞进向量库,而是把图片变成“可检索的信息”。
企业技术支持场景里,图片信息量极大。用户最喜欢发截图:蓝屏、报错弹窗、接口异常堆栈、配置页面。这些截图里的文字比用户复述的“我这边报错了”准确得多,不利用就太可惜了。
我分两种方式处理图片:
- 图片作为知识库资料:上传到对象存储,用 OCR 把图片里的文字抽出来,连同图片地址一起进入元数据。检索时命中文字片段,回答时把原图返回给用户。用户看到“你看这张截图”比纯文字说明直观得多。
- 用户上传图片作为输入:用多模态模型把截图转成结构化文本(识别报错信息、界面关键字),再拿这段文本去走 RAG 检索。实测下来,对“重装系统后登录失败”“某个按钮不存在”这类问题,准确率比单纯让用户描述高好几个档次。
如果团队暂时没有多模态模型,退而求其次可以用通用的 OCR 服务先顶上,核心是不要只存图不提取信息,否则知识库里的图片永远搜不到。
4. Agent 设计与并发实践:从“问答机”到“能干活的助手”
4.1 Agent 框架选型:别一上来就上重型编排
这个项目里我最想分享的教训是:Agent 框架不要迷信“越重越好”。
我们最开始调研了各种 Agent 编排框架,有的支持 ReAct 循环、工具调用、Plan-and-Execute,功能很全。但实际场景里,80% 的技术支持问题根本不需要复杂的工具调用链,只需要“检索知识 → 生成回答 → 附上来源”这个线性流程。
我最终采用了轻量方案:知识问答核心用 LangChain4j 的 Easy RAG 起步,快速验证效果,同时预留了工具调用接口,等真正需要查工单、重置密码时再接上 ReAct 循环。Spring 系的团队选 LangChain4j 很顺手,如果你用的是 Python 技术栈,LangChain 或 LlamaIndex 都行。
判断要不要上完整 Agent 编排有一个标准:看你的任务是不是“需要调用多个工具且工具间有依赖”。比如“先查用户权限 → 再查工单 → 根据结果生成操作建议”就属于这种,值得做 ReAct。反过来,“查一下说明书里怎么配置”这种,做一个 RAG 查询接口就够了,别自己给自己加复杂度。
4.2 让 Agent 感知 Token:把用户身份注入 Prompt 与工具权限
Token 在 Agent 里不只是用来做“登录校验”,更关键的是把身份信息变成模型可见的上下文。我在 Prompt 里放了一段固定的身份块:
你是企业内部技术支持助手,面向公司员工提供产品使用与故障排查帮助。 当前用户信息: - 用户名:{user_name} - 角色:{role} - 部门:{department} - 当前产品范围:{product_scope} - 上下文关联工单:{order_id} 请基于以上身份信息回答问题,不要返回超出用户权限范围的内容。这段信息从哪里来?就是从 JWT 解析出的 claims,加上一次轻量用户服务查询拼出来的。效果非常明显:普通员工问“怎么申请权限”,Agent 会引导他走审批流程;管理员问同样的问题,Agent 会直接给他管理端配置步骤。
“有 Token 更聪明”的另一个落地点是工具权限。Agent 后续接入了“查询工单”工具,调用时会把当前用户的 user_id 透传给下游 API。这样即使有人故意构造 Prompt 让 Agent 查别人的工单,下游接口也会基于透传身份做校验,从权限模型上切断越权风险。总结一句话:Token 决定的不是“能不能回答问题”,而是“能回答到什么程度”。
4.3 并发高峰:AI Agent 怎么扛住突增流量
企业技术支持 Agent 的并发量不算高,但会突然爆发。典型场景是:早上 10 点全员登录遇到故障,几百人同时来问;或者月底报销政策更新,短时间内涌入大量咨询。扛不住的表现就是接口超时、模型调用排队、用户等很久没回复。
我做的几个关键优化:
- 无状态化部署:Agent 服务本身不存任何会话状态,会话上下文放在 Redis,按会话 ID 读取。这样服务可以随便扩副本。
- 热点问题缓存:高频问题(比如“怎么登录”“密码怎么改”)提前把完整回答缓存起来,命中后直接返回,不走 LLM,成本几乎为零。
- 大模型调用池化:模型服务端有并发限制,客户端做了连接池和排队,避免突发流量全部打爆。
- 限流与熔断:对单个用户限流(比如每分钟 5 次提问),超出后返回排队提示,保护下游模型服务和知识库。
另外一个容易被忽略的点是超时与重试。模型生成慢的时候,用户会反复点按钮,造成请求叠加。我的做法是:前端做了幂等标识,同一个问题 30 秒内重复提交直接返回上一次请求的结果或排队状态,杜绝重复消费。
5. Token 与 RAG 联动的常见问题排查实录
5.1 Token 报错速查表:从登录失败到 refresh_token 为空
项目上线后,我们接到最多的报错就是各种 Token 相关异常。我把遇到的问题整理成了一张速查表,基本覆盖了大部分现场:
| 报错信息 | 含义 | 排查方向 |
|---|---|---|
| sign-in could not be completed token exchange failed: error sending request | 客户端向认证中心换 Token 时网络不通或上游不可用 | 检查认证中心健康状态、网关路由、client 配置的 token_endpoint 地址 |
| token exchange failed: token endpoint returned status 403 forbidden | 认证中心拒绝了换 Token 请求 | 检查 client_id / scope / 租户配置,看认证中心风控日志,确认请求头是否带了完整参数 |
| failed to refresh token: 400 bad request: invalid refresh_token: empty string | 刷新时 refresh token 为空 | 查前端存储,refresh token 是否被清掉、请求体字段名是否传错 |
| your access token could not be refreshed because you have since logged out | refresh token 已被登出操作撤销 | 用户需要重新登录,前端要清理本地残留 token |
| your access token could not be refreshed. please log out and sign in again. | refresh token 已失效或轮换过 | 与上一条类似,按业务提示用户重新登录 |
这里面有一条值得单独说:refresh token 为空,看起来是后端报错,根因往往在前端。常见原因有两个,一是刷新 token 存在 localStorage 里但换了域名后被清空;二是刷新接口要求的参数名是refresh_token,前端传了refreshToken,后端拿到空值。把字段名对齐,再把前端 token 存储方案从 localStorage 改成 HttpOnly Cookie(或至少做持久化兜底),问题就能避免。
5.2 RAG 检索质量瓶颈与调优:回答不对别急着怪模型
项目上线后,用户反馈最多的问题是“回答不对、答非所问、说了一堆废话”。多数时候不是模型的问题,而是检索质量没做够。我总结了五类高频问题和对应策略:
| 现象 | 常见原因 | 调优策略 |
|---|---|---|
| 召回为空或极少 | 切块过大导致语义稀释,或查询词太抽象 | 缩小 chunk,增加 query 改写,补充同义词典 |
| 召回了但排序不对 | 只用了向量检索,精确匹配能力弱 | 上混合检索(BM25 + 向量) |
| 答案看着对但出处可疑 | 没有重排,模型被低质量 chunk 带偏 | 加 reranker,或提高元数据过滤强度 |
| 版本/角色信息不对 | 检索结果混入多个版本内容 | 用 Token 身份信息做预过滤,强制版本字段 |
| 回答太长、没有重点 | Prompt 没做约束,模型自由发挥 | Prompt 中明确“先给结论,再给步骤,不超过 200 字” |
最典型的优化案例是版本过滤。我们初期没做版本过滤,用户问“V2.1 怎么配置”,检索结果混进了 V3.0 的文档,答案完全不对。后来在元数据过滤条件里把版本设成必填,再配合 ask 改写(把“新版”映射到具体版本号),准确率直线上升。
5.3 我整理的一份“避坑经验清单”
最后把所有踩过的坑浓缩一下,给后来者直接参考:
- 先做无 Token 的 RAG 基线,再往上加身份感知。不要第一天就搞权限过滤,否则系统会变得极难排错。
- JWT 的密钥要独立管理,定期轮换,网关和认证中心的时钟必须用 NTP 对齐,否则“刚签发就过期”的奇事天天有。
- 知识库一定保留元数据,来源、版本、适用对象缺一不可,这是检索过滤的基石。
- 图片资料单独走 OCR 或多模态链路,不要直接放弃,也不要盲目把原图灌进向量库。
- 并发控制要“限流 + 缓存 + 幂等”三件套一起上,单靠扩机器解决不了 LLM 调用侧的资源瓶颈。
- 线上问题排查时,用请求 ID 串起全网日志:前端 request_id → 网关 jti → 模型调用 trace,没有这个链路,出问题只能靠猜。
写在最后
这个项目做到后面,我最深的体会是:企业级 Agent 的难点从来不在“跑通一个Demo”,而在“让系统在真实组织环境里长期稳定地运行”。我在实际调试中反复确认了一个观点——Token 体系不是锦上添花的安全组件,而是让 RAG 从“能用”变“更聪明”的杠杆。身份信息一旦注入检索过滤、Prompt 上下文、工具权限这三个关键环节,回答质量、合规边界、审计能力都会同步上一个台阶。
如果后面继续扩展,我认为第一个值得做的方向是完善多模态链路(截图识别 + 图片结果返回),第二个是给 Agent 接入会话记忆的自动压缩,让长对话不丢失关键上下文。工具调用和知识检索的组合可以做深度一点,但前提是先把基础链路做稳定,别一上来就铺太大。还是那句话:先把“没 Token 能用”的基线打到 80 分,再让 Token 帮它突破 90 分。