DSPy 默认优化器会在每个预测器上附加最多 20 个演示示例,且每次推理都全部发送,造成严重 token 开销和性能浪费。
DSPy 的卖点是让你停止手写 prompt,让优化器为你编译它们。你用模块写一个程序,给它一个 metric、一个 trainset,运行一个 teleprompter,它就会为每个步骤找到好的 few-shot 示例。它确实有效。教程里没有量化的是"找到好的 few-shot 示例"在每一次调用编译后的程序时需要付出什么代价——不是一次,而是每次。
编译时会附加 demos。每次调用都会重新发送它们。
当你使用默认优化器编译时,DSPy 会引导 few-shot 演示并将它们固定到每个 predictor 上:
# dspy/teleprompt/bootstrap.py
def __init__(self, ..., max_bootstrapped_demos=4, max_labeled_demos=16, ...):
每个 predictor 最多 20 个 demos——4 个引导的(完整的 input→output 轨迹,包括思维链推理过程)加上最多 16 个标注示例。它们存在于编译后的程序上,而不是你写的任何 prompt 字符串里。
然后,在每次推理时,模块会将所有这些 demos 交给 adapter:
# dspy/predict/predict.py
demos = kwargs.pop("demos", self.demos)
self.demos 是优化器附加的完整集合。没有"仅在第一次调用时使用它们"的选项——每次 forward() 默认都是发送整个列表。
"发送 demos"在 token 层面实际上意味着什么
adapter 将每个 demo 转换为一对聊天消息——一个 user 消息和一个 assistant 消息——并将它们全部附加在你的真实输入之前:
# dspy/adapters/base.py — format()
messages.append({"role": "system", "content": system_message})
messages.extend(self.format_demos(signature, demos))
...
# format_demos(), per complete demo:
messages.append({"role": "user", "content": self.format_user_message_content(signature, demo)})
messages.append({"role": "assistant", "content": ...})
所以,一个编译了 12 个 demos 的 predictor 会在每次调用前 prepend 24 条消息。因为引导的 demos 携带完整的推理轨迹,这些消息并不小。这是你在第 1 次请求和第 100 万次请求时都要支付的固定开销——而且它在你的代码里是不可见的,因为你从未写过这些消息。是优化器做的。
而且一个真实的程序有不止一个 predictor
DSPy 的核心意义是组合:一个 pipeline 是多个模块——几个 ChainOfThought 步骤、一个 retriever-reader、一个 router。每个都是一个 predictor,每个都有自己的 demo 集,每次调用时都会重新发送它。每次调用的 prompt 开销大致是:
demos_per_predictor × predictors × (推理轨迹很长,所以每个 demo 都不便宜)
用默认配置编译一个 4 模块的 pipeline,你可能会在单个端用户请求中 prepend 60–80 条 demo 消息——这些在你的源代码中都不存在。
这个旋钮是真实存在的——有意识地设置它
这不是 bug,也不是稻草人:DSPy 在编译时给你提供了控制杆。选择 demo 预算,而不是继承 4 + 16:
from dspy.teleprompt import BootstrapFewShot
optimizer = BootstrapFewShot(metric=my_metric,
max_bootstrapped_demos=2,
max_labeled_demos=2)
compiled = optimizer.compile(program, trainset=trainset)
在发布之前检查实际附加了什么:
for p in compiled.predictors():
print(len(p.demos)) # how many few-shot pairs ride along on every call
要点不是"DSPy 很贵"——而是每次调用重新发送的 demo 数量是优化器替你做的决定,而默认值是慷慨的。
在争论之前先测量它
在调整任何东西之前,先给编译后程序的一次真实运行定价——是算出来的,不是猜的。这就是 @wartzar-bee/tokenscope 做的事(npm i @wartzar-bee/tokenscope):它获取真实的使用量并将每个 bucket——input、output、cache-write(约 1.25×)、cache-read(约 0.1×)——定价为每次运行的真实成本,这样"编译后的 pipeline 成本是 zero-shot 的 N 倍"就不再是一个直觉了。
如果它在 CI 里运行,给它加个门控:wartzar-bee/ci-guardrail 是一个 Apache-2.0 的 GitHub Action(构建在 tokenscope 之上),当一次运行超过绝对的 max-usd 上限时就会失败——所以一个增加 demo 数量的重编译不会在没人注意到的情况下以 3 倍的代价悄然发布。
- uses: wartzar-bee/ci-guardrail@v1
with:
max-usd: "0.50"
如果你运行编译后的 DSPy 程序:每个 predictor 上有多少 demos,每个有多长?在下一张账单替你算之前,值得先给一次真实运行定价。