NOVA:与Jev一起筛选票券、Python规则以及LangChain智能体
一名一级支持助理,负责与Jev一起分析工单,用Python编写业务规则,再由LangChain代理撰写草稿。
有客户写道:“我的订单仍然没有送达。我已经催了三次,明天就必须收到。”在回复之前,客服人员需要先确定几个问题:这个请求该由哪个团队处理?是否需要立即处理?客户的言辞是否真的表达了愤怒?最重要的是,关于这个订单到底能核实些什么?
只需给大型语言模型一个完整的指令,它就能处理所有事务。另一种方法则是将工作分为三个步骤:首先由Jev阅读工单并进行分析,接着用Python代码执行公司的规则,最后由LangChain的客服人员利用相关工具获取信息并起草回复。
此处介绍的项目名为NOVA。它是一款用于支持“Nova Atelier”这一虚构办公用品销售品牌的初级辅助工具。完整代码及笔记本均保存在GitHub的Langchain-Jev仓库中,以下内容仅介绍其中的核心部分。要运行该程序,只需打开笔记本并按顺序执行各单元格即可。
Jev做决策,其他模型负责撰写
在这种架构中,由TypeSafe开发的Jev能够根据用自然语言描述的情况做出结构化的决策。它无需自行处理客户回复相关事宜,人们向它提出的问题通常类似如下:
- 哪个服务部门应该处理这张工单?
- 所报告的情况是否需要立即处理?
- 客户的沮丧程度如何?
回复会出现在结构化的字段中,程序可直接读取这些数据。生成式LLM则会在之后介入,查询可用信息并生成相应的文本。
这种分离方式的优点在于规则清晰可见。要更改触发人工复核的阈值,只需修改Python条件,无需重写整个智能体提示词。不过,仅凭这一点无法证明整个流程的速度或成本会有所提升,只有对包括调用Jev和调用LLM在内的整个工作流进行测量,才能得出结论。
在编写代码之前,工单的流转过程
以下是消息所经历的流程。
Message client + historique
↓
Jev : service, urgence, frustration
↓
Python : validation et règles métier
↓
Agent LangChain + LLM
↓
Consultation des commandes et procédures
↓
Dossier pour l’équipe support + brouillon de réponse
Jev在代理循环之前执行,因此其运行完全不受LLM可能选择的工具影响。该程序随后会根据所选服务、工单优先级以及是否需要人工审核来计算一个方向值。这个方向值会被放入代理的上下文中,与两个只读工具一同存在。
NOVA不会被赋予任何操作权限:既不能发送消息,也不能退款或修改订单。它的职责仅仅是为人工代理准备工作,这一有限的职能范围是经过刻意设计的。
环境搭建
该笔记本主要依赖 langchain、langchain-core、langchain-openai、langchain-typesafe 和 python-dotenv。TypeSafe 的集成版本固定为 0.0.1a3,为重现示例,建议保留安装脚本中的版本号。
需要两把 API 密钥,应将其保存在 .env 文件中:
TYPESAFE_API_KEY=ta_cle_typesafe
OPENAI_API_KEY=ta_cle_openai
JEV_MODEL=jev-latest
OPENAI_MODEL=gpt-4.1-mini
此文件绝不能发布到 GitHub 上,且密钥也不应出现在笔记本的输出结果中。加载环境变量后,这两个模型将如下初始化:
jev = TypeSafeClassifier(
model=os.getenv("JEV_MODEL") or "jev-latest",
timeout=30,
)
modele = ChatOpenAI(
model=os.getenv("OPENAI_MODEL") or "gpt-4.1-mini",
timeout=60,
max_retries=1,
)
这些设置是针对笔记本的,没有强制要求在其他地方使用该大型语言模型。别名 jev-latest 也可能会发生变化。若要对比不同时期的结果,尽可能保留模型的确切版本会更好。
三种基本元素:Choice、Noul 和 Score
理解 Jev 最简单的方法是从提交给它的问题入手。
Choice:选择服务
Choice会从预先定义的类别中选择一个。这里共有四类:配送、账单、技术支持以及其他。每类都有相应的描述,明确说明该分类应涵盖的内容。延迟送达的包裹归入配送类,有争议的付款归入账单类,出现故障的设备则归入技术支持类。其他类别则用于处理那些模糊或超出范畴的请求,避免将其强行归入不合适的服务中。
Noul:一个是非问题
Noul表示阳性回应的概率,范围在0到1之间。需要判断的是,所描述的情况是否需要立即处理而非常规处理。说明中强调不能仅凭语气来判断,因为客户可能非常不满,但其情况未必需要立即干预,而语气平静的请求反而可能十分紧急。该结果仍是基于所提供的上下文做出的估算。在缺乏可靠的配送信息时,它无法判断包裹是否会在明天送达。
评分:将不满程度量化
通过评分功能,可以设定不同的等级:
- 低等级:没有不满迹象;
- 中等级:有轻微的不满,但表达较为克制;
- 高等级:有明显的愤怒情绪,或投诉不断累积。
所使用的评分范围为0到2,可显示小数数值:回答并非简单的“平静”或“愤怒”这类标签。这三个问题被整合在同一个定义中。
from langchain_typesafe import Choice, Noul, Score
def questions_triage():
return {
"service": Choice(
instructions=(
"Quel service doit traiter en priorité la dernière demande du client ? "
"Utilise le contexte seulement pour comprendre cette demande."
),
criteria={
"livraison": "Retard, suivi ou réception d'une commande.",
"facturation": "Paiement, facture ou remboursement.",
"technique": "Panne ou utilisation d'un produit.",
"autre": "Demande ambiguë ou sans rapport avec les catégories précédentes.",
},
),
"urgence": Noul(
instructions=(
"Les faits décrits nécessitent-ils une prise en charge immédiate, "
"plutôt qu'un traitement normal ? Ne te fonde pas seulement sur le ton."
)
),
"frustration": Score(
instructions="Quel niveau de frustration le client exprime-t-il ?",
criteria=[
"Le client s'exprime calmement, sans insatisfaction.",
"Le client exprime une insatisfaction tout en restant mesuré.",
"Le client exprime une forte colère ou des réclamations répétées.",
],
),
}
每个键(服务、紧急程度、 frustration程度)都可用于在代码中查找对应的答案。实际评估是在相关指令和标准中进行的。
第一个需要分析的工单
笔记本中使用的消息如下:AN-1001仍未交付,这是第三次催促,明天必须拿到,我们能做些什么?
在准备阶段,requete_triage函数会整合消息及其上下文,并限制其长度。客户历史记录中的最后三条消息会被发送给Jev,必要时还会进行截断。调用函数并读取结果的流程大致如下。
reponse_jev = jev.invoke(requete_triage(ticket))
print("Service :", reponse_jev.choices["service"].choice)
print("Urgence :", reponse_jev.nouls["urgence"].noul)
print("Frustration sur 2 :", reponse_jev.scores["frustration"].score)
笔记本中记录的输出结果为:
Service : livraison
Urgence : 0.76
Frustration sur 2 : 1.93
这些数值来自已保存的运行结果,无法为后续调用提供任何保证。
另一个值得注意的字段是置信度。在第一个示例中,配送排序的置信度为1.0,但这并不意味着该系统绝对准确。在TypeSafe中,这一数值是根据各种可能响应的概率分布得出的,它反映的是模型对某一响应的偏好程度,而非该响应是否与实际情况相符。
业务规则仍存在于程序中
在使用这些结果之前,normalize_analyse函数会筛选出必要的字段,并特别检查该服务是否确实包含在相关流程中,以及挫败感程度是否符合预设的尺度。最终得到的是一个易于理解的Python字典。挫败感得分并未被转换为其他尺度,而且这里也没有任何内容能够证明该分析是正确的。
随后会应用相应的规则:
def orienter_ticket(analyse, seuil_urgence=0.8, seuil_confiance=0.6):
raisons = []
if analyse["urgence"] >= seuil_urgence:
raisons.append("urgence élevée")
if analyse["frustration"] >= 1.5:
raisons.append("forte frustration")
if analyse["confiance_service"] < seuil_confiance:
raisons.append("service incertain")
if analyse["service"] == "autre":
raisons.append("demande à clarifier") return {
"service": analyse["service"],
"priorite": "haute" if analyse["urgence"] >= seuil_urgence else "normale",
"revue_humaine": bool(raisons),
"raisons": raisons or ["traitement courant"],
}
这些阈值仅具有指导意义。真正的团队会通过具有代表性的工单样本来调整它们,同时记录分流错误以及未被发现的紧急情况。通过完整测试所获得的定位结果如下:
{
"service": "livraison",
"priorite": "normale",
"revue_humaine": true,
"raisons": ["forte frustration"]
}
因此,普通优先级可以与人工审核共存。这里的优先级取决于紧急信号,而挫败感则可能触发重新审核。这两个维度是按需分别处理的。
参考信息
该演示使用了一个简单的错误订单词典。AN-1001指的是一盏仍在运输途中、已延误三天的台灯。操作员拥有两种工具:
- consulter_commande(numero):根据订单编号查找对应订单;
- consulter_procedure(service):读取与所选服务相关的流程。
配送流程要求查看货物状态,若出现延迟需由物流团队进行核实,且绝不能虚构配送日期。在账单处理方面,所有退款都必须经过人工确认。从技术层面来看,操作员需要记录相关产品、发现的故障以及已进行的测试。
NOVA的提示词明确了其任务并规定了输出格式:问题概述、已核实或缺失的信息、建议的下一步行动以及回复草稿。然而,仅在提示词中写明禁止退款并不足以保护支付工具,因此还必须有物理层面的限制:操作员无法接触到任何退款工具。
LangChain操作员的组装
create_agent函数接收LLM、各种工具,以及经过Jev分析并确定方向后的优化后的提示词。
def creer_agent(modele, analyse, orientation):
contexte = json.dumps(
{"analyse_jev": analyse, "orientation": orientation},
ensure_ascii=False,
)
return create_agent(
model=modele,
tools=[consulter_commande, consulter_procedure],
system_prompt=(
ROLE_NOVA
+ "\nContexte de traitement fourni par le programme :\n"
+ contexte
),
)
因此,分析与方向设定并非 create_agent 函数特有的参数,因为该自定义函数会将它们嵌入到基础指令中。traiter_ticket 函数负责协调整个流程:它调用 Jev、标准化分析结果、计算方向、构建智能体,并设置递归限制来启动其循环。
代码规定了各步骤的顺序,但在循环过程中是否调用查询工具则由大语言模型自行决定。如果在实际应用中必须进行查询,最好将其纳入工作流中,而非仅依赖简单的指令。
测试结果所示
保存的第一张工单记录显示,客服人员先使用了“查询订单”功能,随后又使用了“查询流程”功能。他确认了灯具的型号,发现其正在运输中,并记录下了三天的延迟时间。他建议由物流部门进行核查,但并未给出虚假的交货日期,随后为支持团队准备了草稿。
这份草稿需要重新审阅。诸如“我们将为您转交相关文件”之类的表述会让团队误以为已经采取了程序中并未实际执行的操作,而且建议并不等同于实际的转交行为。
第二次尝试是针对客户询问订单包含哪些商品的跟进请求。由于客户没有重复提供产品编号,因此直接调用了第一次处理的记录。
suivi = traiter_ticket(
"Quel article contient cette commande ?",
modele,
jev,
historique=dossier["messages"],
)
print(suivi["reponse"])
在录制的对话中,AN-1001这个编号出现时客户并未重复输入,系统回复的是台灯相关内容。并未出现任何独立的持久化记忆:程序只是重复了之前的消息。对于其他客户,则需要从不同的对话记录开始,因为混合不同客户的对话会导致一个人的信息泄露给另一个人。
正式部署之前
演示版本使用的是虚拟数据,仅限于草稿阶段。在将其接入真实系统之前,需要完成以下几步:
- 通过带有注释的工单来评估分类方式,包括法语注释,然后确定当Jev或大型语言模型未回复时应采取的处理方式;
- 确认谁有权限查看哪些指令,并对不同客户的对话进行隔离;
- 将传输给模型的个人数据减少到最低限度;
- 保留可用的记录,同时不暴露 API 密钥。
此处将 revue_humaine 标志设置为 true 仅是向处理程序提供的一则信息。如果该设备确实会发送消息或执行敏感操作,这一信息就必须触发实际的审批流程。
现在需要牢记的原则就是任务分工。结构化分析由Jev负责,是否使用该分析则由Python决定,而大语言模型则借助相关工具来撰写回复。若要进一步拓展,只需添加各种不同的场景:账单请求、含义模糊的咨询、礼貌但紧迫的消息等。通过观察分类结果并验证规则是否能带来预期的处理结果,既能了解智能体的能力,也能发现仍需改进的地方。
相关阅读
- 用90行Python代码构建迷你RAG流程:检索、增强、生成 —— 利用嵌入向量、余弦相似度及排序列表从零开始构建检索增强生成系统,进而了解简化版本在处理大规模数据时的局限性。