Skip to content

9. 系统能力导览:先认识 Prompt API

写到这里,echo_demo 已经有了 Command、Service、Config 和 Tool。

如果继续只盯着“组件怎么写”,接下来很容易开始觉得插件系统像一堆零件的拼装说明书:这个组件做什么,那个组件做什么,写法都知道了,但这些能力怎么真正组织成一段可用的上下文,反而还是模糊的。

这时候,把视线转到 Prompt API 会很合适。

因为它处理的不是“再加一个组件”,而是另一类问题:

当插件已经开始有能力时,你准备怎样把这些信息组织成一段可用的 prompt。

这一章我们先不碰真正的模型调用,也不急着接入完整对话链。先只做两件事:

  • 认识 PromptTemplate 是怎么注册、取回和渲染的。
  • 认识 system reminder 是什么,以及它在系统里扮演什么角色。

把这两件事看顺了,后面再接 LLM API,你会轻松很多。

9.1 先建立一个最小认知:Prompt API 不等于“直接调模型”

第一次看到 prompt_api,很多人会下意识把它理解成“发起模型请求前的一个辅助工具”。

这么理解不能算错,但还不够准确。

更贴近实际的说法是:

Prompt API 负责组织 prompt 资产,不负责替你完成模型调用。

这里的“prompt 资产”,可以先理解成两类:

  • 模板:一段可复用的 prompt 骨架。
  • 提醒:一段可以被系统保存和取回的补充性上下文。

所以这一章你看到的重点会是:

  • 怎么把一段 prompt 模板放进系统里。
  • 怎么在真正需要的时候把它取出来并填值。
  • 怎么存一段 reminder,供后续流程按需取用。

而真正“把 prompt 发给模型”这件事,我们留到后面的 LLM API 再接上。

9.2 当前这块 API 的导入边界,要先说清楚

前面我们一直在尽量遵循一个原则:

插件作者优先从 src.app.plugin_system 这层导入,而不是直接碰更底层实现。

这一点在 Prompt 这里现在已经顺了一些。当前更推荐这样理解:

  • prompt_api 本身来自 src.app.plugin_system.api
  • PromptTemplate 这类常用类型,可以从 src.app.plugin_system.types 导入

也就是说,这一章的写法会是这样的:

python
from src.app.plugin_system.api import prompt_api
from src.app.plugin_system.types import PromptTemplate

所以这里最稳妥的理解方式是:

  • 模板对象本身,优先从 plugin_system.types 拿。
  • 模板的注册、查询和 reminder 管理,优先走 prompt_api

你也可以顺手记住一件事:plugin_system.types 现在不只是为了这章临时存在的一层。

它更适合被理解成:

  • 插件作者常用类型的统一入口。
  • PromptTemplateChatTypeMessageChatStreamROLE 这类高频类型的位置。

也就是说,后面你在插件里需要拿到运行时消息模型、聊天流模型、常见枚举或 LLM 内容类型时,优先先看看 src.app.plugin_system.types,而不是下意识往 src.coresrc.kernel 里继续翻。

9.3 先给 echo_demo 注册一个模板

我们先不把事情做复杂。

这一章给 echo_demo 加一个最小模板:echo_demo.reply

这个模板不负责真正调用模型,它只负责把插件当前关心的信息组织成一段完整 prompt。比如:

  • 用户输入了什么
  • 当前想要什么格式模式
  • 这段回复应该保持什么风格

可以先在 plugin.py 里加一段注册逻辑:

python
from __future__ import annotations

from src.app.plugin_system.api import prompt_api
from src.app.plugin_system.base import BaseCommand, BasePlugin, cmd_route, register_plugin
from src.app.plugin_system.types import PromptTemplate

from .config import EchoDemoConfig
from .service import EchoService
from .tool import EchoFormatterTool


class EchoCommand(BaseCommand):
    ...


@register_plugin
class EchoDemoPlugin(BasePlugin):
    """最小回显插件。"""

    plugin_name = "echo_demo"
    plugin_description = "一个用于演示插件加载与命令执行的最小插件"
    plugin_version = "1.0.0"

    configs: list[type] = [EchoDemoConfig]

    async def on_plugin_loaded(self) -> None:
        """插件加载完成后注册一个可复用 prompt 模板。"""
        prompt_api.register_template(
            PromptTemplate(
                name="echo_demo.reply",
                template=(
                    "你正在处理 echo_demo 插件的文本任务。\n"
                    "用户输入:{user_input}\n"
                    "格式模式:{mode}\n"
                    "请输出一条简洁、自然、不过度发挥的回复。"
                ),
            )
        )

    def get_components(self) -> list[type]:
        """返回当前插件包含的组件。"""
        if isinstance(self.config, EchoDemoConfig) and not self.config.plugin.enabled:
            return []
        return [EchoCommand, EchoService, EchoFormatterTool]

这里你可以先只抓住一句话:

模板一旦注册,就可以在后续任何需要的地方按名称取回。

这和把一段 prompt 字符串散落在各个函数里,是完全不同的感觉。

前者是在建设“系统里的可复用 prompt 资源”,后者只是临时拼接字符串。

9.4 为什么这里放在 on_plugin_loaded()

这个位置不是唯一选择,但很适合入门阶段理解。

因为它表达得很清楚:

  • 插件被系统正式加载完成。
  • 然后插件把自己需要的 prompt 资源注册进去。

这样读起来有一种很自然的时序感。

你可以把它理解成:

插件启动时,顺手把自己要用的 prompt 资产也准备好。

当然,后面如果你的模板数量很多,或者你希望专门拆一个模块来管理模板,也完全可以再演进。但入门阶段先把注册动作放在插件生命周期里,最容易看懂。

9.5 然后在 Service 里把它取出来

模板注册完之后,下一步不是立刻调模型,而是先把“取回模板并构建 prompt”这一步走通。

这时候很适合把逻辑放进 service.py,因为它本质上还是一段可复用能力:

python
from __future__ import annotations

from src.app.plugin_system.api import prompt_api
from src.app.plugin_system.base import BaseService


class EchoService(BaseService):
    """EchoDemo 的核心回显能力。"""

    service_name = "echo_service"
    service_description = "提供基础的回显与文本处理能力"

    async def build_reply_prompt(self, text: str, mode: str) -> str:
        """构建一段供后续模型使用的 prompt。"""
        template = prompt_api.get_template("echo_demo.reply")
        if template is None:
            return "你正在处理 echo_demo 插件的文本任务。"

        return await (
            template.set("user_input", text)
            .set("mode", mode)
            .build()
        )

这段代码有一个非常值得注意的细节:

python
template = prompt_api.get_template("echo_demo.reply")

拿到的不是全局模板本体,而是一个可供当前调用使用的模板副本。这个设计很重要,因为它意味着你后面继续:

python
template.set("user_input", text)

不会把全局模板污染掉。

这点一旦理解透,你对 PromptTemplate 的使用会踏实很多。因为它不是“大家公用同一份带状态的对象”,而更像“从注册中心取一份当前可操作副本”。

9.6 现在 Command 里可以先把它当成普通文本能力来验证

既然 EchoService 已经能构建 prompt,那命令层就可以先拿它做一个很朴素的验证。

例如在 plugin.py 里给 EchoCommand 补一个路由:

python
class EchoCommand(BaseCommand):
    """最小回显命令。"""

    command_name = "echo"
    command_description = "一个用于演示插件系统的最小回显命令"
    command_prefix = "/"

    async def _get_service(self) -> EchoService:
        """创建当前插件对应的 EchoService 实例。"""
        return EchoService(self.plugin)

    @cmd_route("prompt")
    async def handle_prompt(self, text: str) -> tuple[bool, str]:
        """预览当前 prompt 的构建结果。"""
        service = await self._get_service()
        prompt_text = await service.build_reply_prompt(text=text, mode="plain")
        return True, prompt_text

这样你就可以先通过类似下面的命令,观察模板最终被渲染成什么样子:

text
/echo prompt 你好,帮我写一句自我介绍

这一小步的意义其实很大。

因为它把 Prompt API 从“看起来很抽象的系统能力”,变成了一个你能立刻看到结果的可验证环节。

9.7 如果不想显式注册,也可以先用 get_or_create()

前面我先用了 register_template(),是因为它最能体现“先注册,再取回”的系统视角。

但在很多更轻量的场景里,你也可以直接用 get_or_create()

python
template = prompt_api.get_or_create(
    name="echo_demo.reply",
    template=(
        "你正在处理 echo_demo 插件的文本任务。\n"
        "用户输入:{user_input}\n"
        "格式模式:{mode}\n"
        "请输出一条简洁、自然、不过度发挥的回复。"
    ),
)

它的直觉很好懂:

  • 如果系统里已经有这个模板,就直接拿来用。
  • 如果还没有,就顺手创建并注册。

这个接口很适合“我现在就想先把流程跑起来”的场景。

不过也要顺手记住它和 register_template() 的一个差异:

  • register_template() 更像明确覆盖或明确注册。
  • get_or_create() 更像懒加载初始化。

入门阶段,两种都可以用。只是如果你很在意模板资源的初始化时机,on_plugin_loaded() 里的显式注册会更容易读懂。

9.8 system reminder 是另一种 prompt 资产,但它不是模板

到这里,模板这条线已经清楚不少了。接下来再看另一半:system reminder

它和模板最关键的区别在于:

  • 模板更像可渲染的 prompt 骨架。
  • reminder更像可存取的补充性系统上下文。

你可以把 reminder 想成一段“系统暂存的提醒信息”。它不是为了像模板那样反复填值渲染,而更像:

  • 有一条系统级提示我想先存起来。
  • 某条链路真正需要时,再把它读出来。

这一点在仓库里的真实插件里已经有使用,例如 emoji_sender 就会在插件加载时同步一段 actor reminder。

9.9 先给 echo_demo 加一条最小 reminder

为了让概念更具体,我们也给 echo_demo 补一条很小的 reminder:

python
async def on_plugin_loaded(self) -> None:
    """插件加载完成后注册 prompt 资产。"""
    prompt_api.register_template(
        PromptTemplate(
            name="echo_demo.reply",
            template=(
                "你正在处理 echo_demo 插件的文本任务。\n"
                "用户输入:{user_input}\n"
                "格式模式:{mode}\n"
                "请输出一条简洁、自然、不过度发挥的回复。"
            ),
        )
    )

    prompt_api.add_system_reminder(
        bucket="actor",
        name="echo_demo_style",
        content="回复风格保持简洁、温和,不要为了显得聪明而过度展开。",
    )

这样做之后,这条 reminder 就被存进系统里了。

后面如果你想把它取出来,也可以这样写:

python
reminder_text = prompt_api.get_system_reminder("actor", ["echo_demo_style"])

如果当前 bucket 下有内容,它会返回类似这样的文本:

text
[echo_demo_style]
回复风格保持简洁、温和,不要为了显得聪明而过度展开。

9.10 这里有一个特别容易误会的点:reminder 不会自动注入

这个地方我建议你务必要记住,因为它特别容易产生“我不是已经存进去了吗,为什么模型没有自动用上”的错觉。

当前 prompt_api.add_system_reminder() 做的事情,本质上只是:

把 reminder 存起来。

它不会自动替你把这段内容塞进某一次 LLM 请求里。

也就是说,正确理解应该是:

  • add_system_reminder() 负责存储。
  • get_system_reminder() 负责取回。
  • 至于什么时候把它拼进 prompt 或注入到模型上下文,要由后续调用链自己决定。

这个设计其实很合理。因为 reminder 本来就可能服务于很多不同链路,而不是一存进去就强行污染所有请求。

9.11 现在你可以怎么理解 Prompt API

如果把这一章的重点压缩一下,Prompt API 现在可以先这样理解:

  • 它不是模型调用器。
  • 它更像 prompt 资源的管理入口。
  • 模板适合承载“可复用、可填值、可渲染”的 prompt 骨架。
  • reminder 适合承载“先存起来、按需取用”的补充上下文。

而对插件作者来说,这一层最大的价值是:

你终于不用把 prompt 当成一堆散落在业务代码里的长字符串了。

这一步看起来没有“模型真的回答了”那么刺激,但它其实是在给后面的系统协作打地基。

9.12 这一章先停在这里,刚好

我刻意没有在这一章里继续往下冲到 LLM 请求。不是因为它不重要,而是因为如果把 Prompt API 和 LLM API 一口气揉在一起,第一次读会很容易把边界看花。

这一章你只要先把两件事记牢,就已经够了:

  1. PromptTemplate 负责把一段 prompt 组织成可复用模板。
  2. system reminder 负责把补充性上下文存起来,后续按需取回。

下一步如果继续往前走,就很自然了:

既然 prompt 已经能被组织出来了,那它接下来该怎么进入真正的模型请求?

那就是 LLM API 要接上的部分。

贡献者

The avatar of contributor named as minecraft1024a minecraft1024a
The avatar of contributor named as Windpicker-owo Windpicker-owo

页面历史

Released under the GPL-3.0 License.

新对话
MoFox 助手

下午好。

今天想做点什么?

聊天内容可能会被记录以用于改进服务及其质量,并会遵循我们的隐私政策进行处理。