首页 / 文章

NOVA:与Jev一起筛选票券、Python规则以及LangChain智能体

一名一级支持助理,负责与Jev一起分析工单,用Python编写业务规则,再由LangChain代理撰写草稿。

发布者:REACTAPP.TOP阅读约 10 分钟2331 词

来源: Trier les tickets avec Jev, appliquer ses règles en Python et laisser le LLM préparer la réponse : un tutoriel concret avec NOVA.

有客户写道:“我的订单仍然没有送达。我已经催了三次,明天就必须收到。”在回复之前,客服人员需要先确定几个问题:这个请求该由哪个团队处理?是否需要立即处理?客户的言辞是否真的表达了愤怒?最重要的是,关于这个订单到底能核实些什么?

只需给大型语言模型一个完整的指令,它就能处理所有事务。另一种方法则是将工作分为三个步骤:首先由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决定,而大语言模型则借助相关工具来撰写回复。若要进一步拓展,只需添加各种不同的场景:账单请求、含义模糊的咨询、礼貌但紧迫的消息等。通过观察分类结果并验证规则是否能带来预期的处理结果,既能了解智能体的能力,也能发现仍需改进的地方。

相关阅读

  • 使用 SmolLM3 的本地支持代理:思考模式、工具与限制机制 — 作为支持代理在笔记本电脑上运行 30 亿参数模型:何时使用思考模式或 no_think 模式,工具调用机制是怎样的,以及如何防止调用占位符工具。