两种写法:字符串 vs 类
字符串签名(快速原型)
import dspy qa = dspy.Predict("question -> answer") print(qa(question="法国的首都是?").answer) # 巴黎
更复杂:
summarize = dspy.Predict("document -> title, summary, keywords") out = summarize(document=long_text) print(out.title, out.keywords)
-> 左边是输入字段,右边是输出字段,逗号分隔。适合 30 秒内写完一个 demo。
类签名(生产首选)
from typing import Literal class SentimentAnalysis(dspy.Signature): """判断客服对话里用户的情绪是否激动。""" dialogue: str = dspy.InputField(desc="用户与客服的多轮对话,每行一条") emotion: Literal["calm", "upset", "angry"] = dspy.OutputField( desc="calm=平和, upset=有抱怨, angry=显著愤怒" ) reason: str = dspy.OutputField(desc="一句话解释判断依据")
类签名有三项字符串签名做不到的事:
- docstring:整个任务的简短描述,会进 system prompt
- 类型:
Literal[...]/int/list[str],DSPy 自动校验 - desc:每个字段的专有说明,影响 LLM 如何填那个字段
DSPy 底层做了什么
当你写 dspy.Predict(SentimentAnalysis),DSPy 会生成类似这样的 prompt(简化版):
判断客服对话里用户的情绪是否激动。
---
Follow the following format.
Dialogue: 用户与客服的多轮对话,每行一条
Emotion: calm=平和, upset=有抱怨, angry=显著愤怒
Reason: 一句话解释判断依据
---
Dialogue: {{对话文本}}
Emotion:
你不需要自己写这些——DSPy 根据 Signature 拼出来。换个 adapter(ChatAdapter/JSONAdapter)还能换成 JSON 结构化输出。
InputField 和 OutputField 详解
常用参数
desc: str
对这个字段的描述。会被写进 prompt,是 LLM 理解字段含义的唯一依据——desc 越清晰,输出越稳。
prefix: str
字段在 prompt 里的标题名,默认就是字段名首字母大写。一般不用改。
format: Callable
自定义字段渲染函数。比如把 list 转成带序号的字符串。
类型注解的威力
from pydantic import BaseModel class Address(BaseModel): province: str city: str street: str class ExtractAddress(dspy.Signature): """从文本中提取结构化地址""" text: str = dspy.InputField() address: Address = dspy.OutputField() # DSPy 自动生成 JSON schema
上面这个例子,DSPy 会:
- 在 prompt 里把 Pydantic schema 转成 JSON 描述
- 调用 LLM 时要求按 JSON 格式输出
- 收到输出后用 Pydantic parse,parse 失败自动重试
字符串签名的进阶语法
可以加字段类型注解(2.5+):
cls = dspy.Predict("text -> label: Literal['pos','neg']") extract = dspy.Predict("text -> entities: list[str], relations: list[tuple]")
多输入多输出
class ResolveTicket(dspy.Signature): """根据工单内容、用户历史、知识库片段给出处理建议""" ticket: str = dspy.InputField() user_history: list[str] = dspy.InputField(desc="用户过去 30 天的 5 条工单摘要") kb_snippets: list[str] = dspy.InputField(desc="检索到的 top-3 知识库片段") category: Literal["bug", "feature", "billing", "other"] = dspy.OutputField() priority: Literal["p0", "p1", "p2"] = dspy.OutputField() suggested_reply: str = dspy.OutputField(desc="给客服看的回复草稿,500 字以内")
DSPy 会保证三个输出字段都有,缺任何一个会触发重试或报错(可配)。
desc 怎么写才高效
给 desc 写"约束"而不是"任务"
❌
✅
❌
desc="这是文章摘要"(LLM 知道是摘要,废话)✅
desc="不超过 80 字,不用问号和感叹号,包含核心数字"(给了边界条件)
好的 desc 通常包含:
- 长度/格式约束(必需)
- 取值范围(对 int/float 字段)
- 与输入关系的提示("必须引用 context 中的证据")
- 反例("不要输出 '无法判断',给出最可能的答案")
继承 Signature:复用字段定义
class QAInputs(dspy.Signature): context: str = dspy.InputField() question: str = dspy.InputField() class SimpleQA(QAInputs): """只给答案""" answer: str = dspy.OutputField() class QAWithCitations(QAInputs): """给答案 + 引用""" answer: str = dspy.OutputField() citations: list[str] = dspy.OutputField(desc="引用的 context 片段原文")
动态生成签名
运行时拼字段名:
def make_extractor(fields: list[str]): sig = "text -> " + ", ".join(fields) return dspy.Predict(sig) extractor = make_extractor(["name", "email", "phone"])
常见坑
| 坑 | 症状 | 解法 |
|---|---|---|
| 字段名用中文 | 部分模型报错 | 字段名保持英文 snake_case,desc 写中文 |
| 忘写 desc | LLM 输出格式飘 | 即便是 30 秒原型,desc 也要写关键约束 |
| 输出类型太宽(str) | Optimizer 难用 | 能 Literal 就 Literal,能结构化就用 Pydantic |
| docstring 过长 | token 浪费 | docstring 写 1-2 句"总目标",细节放到各字段 desc |
本章小结
- 字符串签名快速原型,类签名生产首选——带 docstring、类型、desc
- DSPy 根据 Signature 自动拼 prompt,你不需要手写
- desc 写约束(长度、取值、禁用),不写重复任务描述
- 用 Literal / Pydantic 做结构化输出,比 str 稳且易于优化
- Signature 可以继承、可以在运行时动态生成