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.apiPromptTemplate这类常用类型,可以从src.app.plugin_system.types导入
也就是说,这一章的写法会是这样的:
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 现在不只是为了这章临时存在的一层。
它更适合被理解成:
- 插件作者常用类型的统一入口。
- 放
PromptTemplate、ChatType、Message、ChatStream、ROLE这类高频类型的位置。
也就是说,后面你在插件里需要拿到运行时消息模型、聊天流模型、常见枚举或 LLM 内容类型时,优先先看看 src.app.plugin_system.types,而不是下意识往 src.core 或 src.kernel 里继续翻。
9.3 先给 echo_demo 注册一个模板
我们先不把事情做复杂。
这一章给 echo_demo 加一个最小模板:echo_demo.reply。
这个模板不负责真正调用模型,它只负责把插件当前关心的信息组织成一段完整 prompt。比如:
- 用户输入了什么
- 当前想要什么格式模式
- 这段回复应该保持什么风格
可以先在 plugin.py 里加一段注册逻辑:
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,因为它本质上还是一段可复用能力:
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()
)这段代码有一个非常值得注意的细节:
template = prompt_api.get_template("echo_demo.reply")拿到的不是全局模板本体,而是一个可供当前调用使用的模板副本。这个设计很重要,因为它意味着你后面继续:
template.set("user_input", text)不会把全局模板污染掉。
这点一旦理解透,你对 PromptTemplate 的使用会踏实很多。因为它不是“大家公用同一份带状态的对象”,而更像“从注册中心取一份当前可操作副本”。
9.6 现在 Command 里可以先把它当成普通文本能力来验证
既然 EchoService 已经能构建 prompt,那命令层就可以先拿它做一个很朴素的验证。
例如在 plugin.py 里给 EchoCommand 补一个路由:
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这样你就可以先通过类似下面的命令,观察模板最终被渲染成什么样子:
/echo prompt 你好,帮我写一句自我介绍这一小步的意义其实很大。
因为它把 Prompt API 从“看起来很抽象的系统能力”,变成了一个你能立刻看到结果的可验证环节。
9.7 如果不想显式注册,也可以先用 get_or_create()
前面我先用了 register_template(),是因为它最能体现“先注册,再取回”的系统视角。
但在很多更轻量的场景里,你也可以直接用 get_or_create():
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:
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 就被存进系统里了。
后面如果你想把它取出来,也可以这样写:
reminder_text = prompt_api.get_system_reminder("actor", ["echo_demo_style"])如果当前 bucket 下有内容,它会返回类似这样的文本:
[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 一口气揉在一起,第一次读会很容易把边界看花。
这一章你只要先把两件事记牢,就已经够了:
- PromptTemplate 负责把一段 prompt 组织成可复用模板。
- system reminder 负责把补充性上下文存起来,后续按需取回。
下一步如果继续往前走,就很自然了:
既然 prompt 已经能被组织出来了,那它接下来该怎么进入真正的模型请求?
那就是 LLM API 要接上的部分。

