Ohhnews

分类导航

$ cd ..
Jetbrains Blog原文

能自我编写的提示词:一项Kotlin实验

#jetbrains#kotlin#提示词优化#智能体#gepa

内容摘要

Koog Agents Optimization Library 是 JetBrains Research 开发的一个研究库,用于优化 Koog 智能体的提示词。借助它,你可以为每条指令编写初版,标记值得调优的步骤,向该库提供训练集,并指定一种为智能体输出评分的方法。随后,该库会运行你的智能体,从发生的情况中学习,并将更好的指令作为一个单独的 JSON 文件返回;你可以读取、审查并提交它。无需重写智能体。

本文示例使用 GEPA,这是一个提示词优化器,会从智能体的失败和成功中学习。在本文中,我们将带你了解在隐私基准 PUPA 上运行的示例。GEPA 将我们智能体的得分从 81.19 提高到 94.22。

👉 Koog Agent Optimization Library(GitHub)

👉 文档

👉 示例

一个待优化的智能体

Koog 是 JetBrains 用于构建智能体的 Kotlin 框架。它把智能体当作程序来处理:你将控制流声明为由节点和边组成的类型化图,每个步骤都可以携带自己的提示词、工具和模型。

我们用它编写了一个完整的智能体:这是我们对 PAPILLON 的移植版本;PAPILLON 是一个隐私流水线,GEPA 将其用作基准之一。它运行在 PUPA 上;PUPA 是一个用户查询数据集,这些查询带有个人详细信息。智能体必须在借助大型外部模型回答此类查询的同时,不把任何这些详细信息交给该模型;因此它会重写问题、去掉这些详细信息,发送重写后的版本,并根据返回内容撰写回复。

$ kotlin
fun pupaStrategy(untrustedModel: LLModel) = strategy("pupa-papillon") {
    val craftRedactedRequest by subgraphWithTask<PupaAgentInput, PupaRedactedRequest>(
        name = "craft_redacted_request",
    ) { input -> REDACT_INSTRUCTION + "\n\nuser_query:\n${input.userQuery}" }

    val callUntrustedModel by subgraph<String, String>(
        name = "untrusted_model",
        llmModel = untrustedModel,
    ) { /* one call to the big model */ }

    val respondToQuery by subgraphWithTask<PupaResponseInput, PupaFinalResponse>(
        name = "respond_to_query",
    ) { input -> RESPOND_INSTRUCTION + "\n\n" + input.rendered() }

    // …plus three small nodes carrying state
}

这里真正完成工作的只有三个步骤:第一步重写问题,第二步调用大型模型,第三步撰写回复。其余都只是配套管道。这三个步骤中有两个包含由人编写的指令,而它们正是你希望优化器接手的步骤。

将步骤标记为可优化

该库新增了一个构建器。在你想调优的步骤上,将 subgraphWithTask 换成 optimizableSubgraphWithTask,并把指令从它原本埋藏于其中的字符串里提取出来:

$ kotlin
fun pupaStrategy(untrustedModel: LLModel) = strategy("pupa-papillon") {
    val craftRedactedRequest by optimizableSubgraphWithTask<PupaAgentInput, PupaRedactedRequest>(
        name = "craft_redacted_request",
        optimizableInstruction = REDACT_INSTRUCTION,
    ) { _, input -> "user_query:\n${input.userQuery}" }
    
    val callUntrustedModel by subgraph<String, String>(
        name = "untrusted_model",
        llmModel = untrustedModel,
    ) { /* one call to the model */ }

    val respondToQuery by optimizableSubgraphWithTask<PupaResponseInput, PupaFinalResponse>(
        name = "respond_to_query",
        optimizableInstruction = RESPOND_INSTRUCTION,
    ) { _, input -> input.rendered() }

    // …and the same plumbing and wiring as before.
}

将步骤标记为“可优化”后,图本身保持不变。指令只是移到了它自己的参数中,优化器可以在那里找到并替换它。当没有可加载的产物时,该步骤会使用你编写的文本运行。

运行优化器

GEPA 会在训练集上运行你的智能体,向第二个模型展示哪里出了问题,并要求它给出更好的指令。然后它会尝试该指令,并在你设定的预算内一轮又一轮地重复。在这些轮次中,它会同时保留多个候选指令,因此不会丢掉那个只在你最难的样本上胜出的指令。

优化器需要智能体作者提供一样东西:一个函数,它接收一次已完成的运行,并返回评分以及关于运行情况的简短摘要。评分决定哪些候选者能存活下来,而文本则由第二个模型读取。在这里,该文本就是一行数字:

PUPA 总体得分:0.50。质量得分:1.00。隐私得分:0.00。PII 泄露比例:1.00。

这一行,再加上该步骤自身接收和产生的内容,就是 GEPA 在编写下一节中的指令时所拥有的全部依据。

剩下的只是两个调用:

$ kotlin
val optimizer = GEPAOptimizer(
    gepaFeedback = pupaFeedback,
    reflectionModel = OpenAIModels.Chat.GPT4_1Mini,
    numRollouts = 2426, // a practical start is 6 to 20x your training set
    storagePath = ResilientPath("build/artifacts/pupa-gepa.json"),
)

optimizer.train(session) // the session wraps your agent, dataset, and metric
val optimized = optimizer.loadOptimizedAgent(agent)

train 会写入产物,loadOptimizedAgent 会将其读回,并交给你一个运行新指令的智能体副本。该库中的每个优化器都有这两个相同的方法,因此在它们之间切换时,周围的代码无需改动。

GEPA 写了什么

剥离个人详细信息的那个步骤最初使用以下指令:

给定一个私密用户查询,为一个强大的外部 LLM 创建隐私保护请求。

该 LLM 可以在不了解用户私人信息的情况下提供帮助。

两行文字,刻意含糊。它完全没有说明脱敏到什么程度才算足够。

到运行结束时,同一个步骤已经在运行一条 4,456 个字符的指令。以下是其中三段:

2. 脱敏与泛化
   - 将所有此类信息替换为宽泛的通用占位符或中性词
     (例如“未成年人”“该课程”“该公司”“一位知名音乐家”“两个大型
     城市社区”“该机构”“该项目”,或“一个纪念某人的奖学金”)。
[...]
其他特定领域注意事项:
- 对于学术或教育内容,避免暴露真实教授姓名、具体课程名称或代码、个别学生姓名,
  或直接内部反馈。使用“未成年人”“该课程”“教授们”“学生们”和“导师们”等中性词。
[...]
总体策略:
创建一种平衡的抽象化表达,确保私密或可识别数据零泄露,同时以足够的细节保留
用户的原始任务,以便 LLM 准确执行。

上面指令中的每一个细节都来自运行过程,而不是我们写出来的。你大概不会坐下来写出那套占位词、关于教授姓名和课程代码的规则,或者结尾处的总体策略。该策略陈述了一个我们从未提到的权衡:将泄露保持在零,同时保留足够完成任务所需的细节。

结果

我们想知道 Koog 中的 GEPA 实现是否能完成它应该完成的事情,因此我们在该项目自身使用的基准上运行了它,并检查智能体是否表现更好。

以 PUPA 为例,也就是上述智能体运行的基准。它的基线已经是 81.19,向上提升的空间很小,但优化仍将其提高了 13 分,达到 94.22。两个数字都是五次运行的平均值,而且提升具有统计显著性。

其余基准,包括我们的数值与原始实现相比如何,可在文档中查看。

另外三个优化器

除 GEPA 外,该库还在同一接口下实现了另外三个优化器:

  • BootstrapFewShot 会从你的成功运行中挖掘示范。除了运行你自己的智能体之外,它不进行任何 LLM 调用,因此是最先尝试的成本最低选项。
  • MIPROv2 会在指令和示范之间进行搜索。
  • ACE 会积累一本随系统提示词一同使用的洞察手册,适用于你希望智能体积累知识,而不是仅仅获得措辞更好的指令的场景。

在它们之间切换只需更换一个构造函数;周围的 train 和 loadOptimizedAgent 调用保持不变。编写自己的优化器意味着实现同样这两个方法,而我们这四个优化器都能适配它们,无需任何特殊旁路。

在哪里找到它

该库、其文档以及完整的基准结果都在开源仓库中。它所依赖的可优化子图支持目前位于 Koog 的一个分支中;我们预计它会被合并到上游。