跳到主要内容

LLM 即评委:判官 Descriptor、Prompt 模板与异步批量 LLM 引擎

30 秒导读: 有些质量问题没有现成公式——"这段回答礼貌吗?""检索到的上下文和问题相关吗?"。 Evidently 的做法是再请一个 LLM 当评委(LLM-as-a-judge):给它一段说明和评分标准,让它给 每一行输出打个类别 / 分数 / 理由。本章讲清这条链路的三层:判官 Descriptor(把数据喂进去)、 Prompt 模板(声明式拼出判官提示词)、异步批量 LLM 引擎(并发跑、按 token 限流、多 provider 适配)。

本章是 Evidently 系列的第 3 章。它只讲评估计算本身(怎么用 LLM 算出分数)。上下游请看:


1. 这是什么(零基础也能懂)

一句话定义: LLM 即评委 = 把"打分"这件事外包给一个 LLM——你写一段评分说明,系统把每一行 待评文本塞进这段说明发给模型,模型回一个结构化判决(类别、分数、理由),系统再把判决落回成数据列。

它解决什么问题。 传统指标(准确率、BLEU、正则匹配)只能测"能被公式算的东西"。可是很多 LLM 应用的质量是语义的、主观的:

想评的问题为什么公式测不了
客服回复够不够礼貌"礼貌"没有数值定义
回答有没有答非所问要理解问题和答案的语义关系
RAG 检索的上下文相不相关要判断一段文字对一个问题"有没有用"
生成文本是不是无害/合规要按一套标准做分类判断

这些问题人来判很自然,于是就让另一个 LLM 模仿人来判。

用起来什么样。 用户只需声明一个"判官"(descriptor),挂到数据集上:

# 示意,非源码 —— 用自定义 prompt 让 LLM 判"回答是否礼貌"
from evidently.descriptors import GenericLLMDescriptor
from evidently.llm.models import LLMMessage

judge = GenericLLMDescriptor(
provider="openai", model="gpt-4o-mini",
input_columns={"answer": "response"}, # 把数据列 response 映射进 prompt 变量 answer
prompt=[LLMMessage.user("这段回答礼貌吗?只回 POLITE 或 RUDE:\n{answer}")],
alias="politeness",
)
# 之后 judge 会给数据集每一行加一列 "politeness"

一句话直觉: 把它想成给数据集雇了一个批改作业的助教——你写一份评分标准(prompt),助教 (LLM)对着每一份作业(每一行)打个分,你拿到一列分数。本章就是拆开"这个助教内部怎么运转"。

本节不出现底层代码细节。目标:完全不懂的人读完知道"这是干嘛的"。


2. 顶层全景(它大概怎么转)

一次 LLM 评估,数据要穿过三层。先看这张流程图,再逐层拆。

读法:从左到右是一条评估的生命周期。数据集每一行独立生成一个请求,
到中间的引擎里并发汇合、限流,再各自把判决落回成列。

第 1 层:判官 Descriptor 第 2 层:Prompt(模板) 第 3 层:异步批量引擎
───────────────────── ────────────────────── ─────────────────────

Dataset 每一行 LLMWrapper.run_batch
(一条待评文本) 每行 → 一个 LLMRequest 并发(Semaphore)+ 限流
│ { messages, response_parser, │
│ ①iterate_messages ───▶ response_type } ──②──▶ │──③─▶ complete()
│ (行数据填进模板) │ └HTTP▶ 模型
│ │◀─④── 原始字符串
▼ │
DatasetColumn(s) ◀──────────────⑤ response_parser 解析成 dict ◀───────┘
alias / alias score / alias reasoning ... 再按输出列拆开

三层各干什么(部件 → 职责 → 文件):

部件职责主文件
判官GenericLLMDescriptor / LLMEval / ContextRelevance把数据集每行变成请求、批量跑、把判决拆成多列src/evidently/descriptors/llm_judges.py_context_relevance.py
PromptBaseLLMPromptTemplate(分类模板)、PromptBlock 积木声明式拼出"带类别定义 / 要 JSON / 带 reasoning"的判官提示词,并给出解析器src/evidently/llm/templates.pyllm/utils/templates.pyllm/utils/blocks.py
引擎LLMWrapper 及各 provider 子类异步并发执行、按 token 估算限流、重试、多 provider 适配src/evidently/llm/utils/wrapper.py

主线走一遍(高层):

  1. 判官的 generate_data 被调用,它对数据集每一行调 iterate_messages,产出一串 LLMRequest ——每个请求带要发的消息怎么解析回复期望返回类型
  2. 这串请求交给引擎的 run_batch_sync,引擎并发发出去(受信号量和限流器约束)。
  3. 每个请求到 provider(OpenAI / Anthropic / …)拿到原始字符串,用请求自带的 response_parser 解析成结构化结果(比如 {"category": "POLITE", "score": 0.9, "reasoning": "..."})。
  4. 判官把这些结果按输出列拆开,每个字段变成一列 DatasetColumn,列名形如 politenesspoliteness scorepoliteness reasoning

目标:看懂"大盘"。下面逐层进代码。


3. 第一层:判官 Descriptor

判官是 Descriptor 的子类(基类见 01-data-model.md)。它必须实现 generate_data(dataset, options):输入整张表,输出一列或多列。LLM 判官有两种写法—— 一种给你完全自定义的 prompt,一种给你结构化的分类模板。

3.1 GenericLLMDescriptor —— 自定义 prompt 的判官

src/evidently/descriptors/llm_judges.py:33GenericLLMDescriptor 是最灵活的判官:你自己写 prompt(纯文本或一串消息),它负责喂数据、批量跑、收结果。四个关键字段:

字段作用
input_columns: Dict[str,str]prompt 变量名 → 数据集列名的映射({"answer": "response"})
provider / model用哪个 provider 的哪个模型
prompt: PromptContentprompt 本体(文本 / 消息列表 / 模板)

第一步:每行数据 → 一个请求。 iterate_messages(llm_judges.py:74)遍历数据集,把 input_columns 指定的列取出、重命名成 prompt 变量,逐行 format 进消息,产出 LLMRequest:

# 真实源码摘要 llm_judges.py:74-83(iterate_messages)
for _, column_values in (
dataset.as_dataframe()[list(self.input_columns)].rename(columns=self.input_columns).iterrows()
):
yield LLMRequest(
messages=self._fmt_messages(cast(Dict[str, Any], column_values.to_dict())),
response_parser=self.prompt.get_parser(), # 怎么把回复解析成结构化
response_type=self.prompt.get_response_type(), # 期望返回 str 还是 dict
)

这段的关键:每个 LLMRequest自带解析器的——请求不光携带"发什么",还携带"回来怎么读"。 解析器从 prompt 上取(content.py:55/131):纯文本 prompt 的解析器是恒等函数(原样返回字符串), 模板 prompt 的解析器则来自模板本身(见 §4)。

第二步:批量跑 + 拆列。 generate_data(llm_judges.py:85)一行就把整批请求交给引擎:

# 真实源码摘要 llm_judges.py:88(generate_data 起手)
result = self.get_llm_wrapper(options).run_batch_sync(requests=self.iterate_messages(dataset))

run_batch_sync 是引擎的同步入口(见 §5)。拿到 result 后,判官根据结果形状决定输出几列 (llm_judges.py:89-116):

  • 结果是一批 dict(结构化判决)→ 转成 DataFrame,每个字段拆成一列,列名 f"{alias} {col}" (llm_judges.py:99-105)。若 prompt 是模板,列类型来自模板声明(get_type);否则一律当文本列。
  • 结果是模板的单主列 → 按模板主输出列的类型(数值就 astype(float))包成一列 (llm_judges.py:106-115)。
  • 其它 → 一列文本(llm_judges.py:116)。

一句话:判官不关心分数怎么算,它只管把数据喂进去、把结构化判决摊平成表格列。

3.2 LLMEval —— 模板驱动的判官

llm_judges.py:119LLMEval 面向结构化分类场景:你不写自由 prompt,而是给一个 template(见 §4 的分类模板),它内部委托给 legacy 的 LLMJudge 特征来跑:

# 真实源码摘要 llm_judges.py:134-146(_judge 属性:把 LLMEval 桥到底层 LLMJudge)
@property
def _judge(self):
from evidently.legacy.features.llm_judge import LLMJudge
return LLMJudge(display_name=self.alias, provider=self.provider, model=self.model,
input_column=self.input_column, input_columns=self.input_columns,
template=self.template)

generate_data(llm_judges.py:156)让这个 judge 生成特征,再把每个输出列按类型转成 DatasetColumn(数值列用 pd.to_numeric(..., errors="coerce") 容错,llm_judges.py:148-154)。 list_output_columns(llm_judges.py:168)提前告诉框架"这个判官会产出哪几列",让上层能预排版。

两种判官怎么选:

GenericLLMDescriptorLLMEval
prompt完全自定义(文本/消息)结构化分类模板
典型输出你 prompt 让它返回什么就什么category / score / reasoning
适合灵活、一次性的自定义判官标准二分类/多分类评估
拆列逻辑按结果形状动态决定按模板 list_columns()

3.3 进阶例子:ContextRelevance —— 一个多列 RAG 判官

src/evidently/descriptors/_context_relevance.py:205ContextRelevance 是判官里最复杂的一个: 评估 RAG 场景下"检索到的每一段上下文,对问题到底相不相关"。它的难点在于一行有多段上下文 (contexts 是一列 list),要逐段打分再聚合。

它把三件事拼在一起:

  1. 两种打分后端(_context_relevance.py:192METHODS):
    • semantic_similarity——用 sentence-transformers 算问题与每段上下文的余弦相似度(:29),不花 LLM;
    • llm——真正的 LLM 判官(llm_scoring,:58),下面重点看。
  2. LLM 打分(llm_scoring,:58):它把 (问题, 每段上下文) 展开成多行,喂给一个 BinaryClassificationPromptTemplate(target=RELEVANT / non-target=IRRELEVANT,:79-101), 然后 template.iterate_messages(...) 生成请求、llm_wrapper.run_batch_sync(...) 批量跑 (:103-104),最后把每行得到的 score 按原始行 index 重新聚回 list(:107-111)。
  3. 聚合(AggregationMethod,:122):一行有多段上下文的多个分数,要压成一个数。三种策略:
聚合类语义输出
MeanAggregation(:133)所有段分数取平均数值
HitAggregation(:147)只要有一段 ≥ 阈值(默认 0.8)就算命中0/1
HitShareAggregation(:164)命中段占比比例

generate_data(:244)把"打分方法 + 聚合方法"组装起来跑,主输出一列聚合分数, output_scores=True 时额外输出每段的原始分数列(:263-267)。

这个例子的价值: 它展示了判官层可以做得多复杂——先展开、再逐条 LLM 打分、再收拢聚合—— 而底层依然是同一套"prompt 模板 + run_batch_sync"。判官层负责"数据怎么切怎么拼",引擎层只管并发。


4. 第二层:Prompt 模板机制

自由文本 prompt 简单,但结构化判官(要模型返回带类别、分数、理由的 JSON)如果靠手写字符串会很脆。 Evidently 用一套声明式积木来拼判官 prompt:你声明"要哪些字段、类别怎么定义",系统生成提示词文本, 并配套生成对应的解析器。核心是三层:分类模板 → BlockPromptTemplate → PromptBlock 积木。

4.1 PromptBlock —— 提示词的乐高积木

src/evidently/llm/utils/blocks.py:21PromptBlock 是最小单位。妙处在 render() (blocks.py:34):它把类的 docstring 当模板,把同名字段值填进 {占位符}。看几个积木:

积木渲染成定义
SimpleBlock一段原样文本blocks.py:97
Anchor{start}\n{block}\n{end} 把内容夹在标记间blocks.py:77
Tag<name>\n{block}\n</name> 包成 XML 标签blocks.py:88
JsonOutputFormatBlock"把这些字段格式化成 JSON 返回…"blocks.py:155

输出格式积木最关键,因为它同时管两件事:生成"请返回 JSON"的指令,以及解析回复JsonOutputFormatBlock(blocks.py:155)继承自 OutputFormatBlock(blocks.py:122),后者要求 子类实现 parse_response。JSON 版的解析很鲁棒(blocks.py:175):先直接 json.loads,失败就 调用 find_largest_json(blocks.py:139)——用正则在模型的啰嗦回复里捞出最大的那段合法 JSON, 容忍模型在 JSON 前后加废话。

# 真实源码摘要 blocks.py:175-183(JSON 解析容错)
try:
return json.loads(response)
except json.JSONDecodeError as e:
if self.search_for_substring:
sub = find_largest_json(response) # 从"…这是结果:{…}谢谢"里抠出 {…}
if sub is not None:
return sub
raise LLMResponseParseError("Failed to parse response as json", response) from e

4.2 BlockPromptTemplate —— 把积木串成一份 prompt

src/evidently/llm/utils/templates.py:128BlockPromptTemplate 负责把一串积木拼成完整模板。 prepare()(templates.py:143)做两件事:把每个积木 render() 后用换行拼起来;并从中挑出那个 OutputFormatBlock 记下来——因为解析回复要用它。get_parser()(templates.py:110)返回的解析器 就是包了一层 key 校验的 output_format.parse_response

这就是 §3 里 iterate_messages 塞进 LLMRequestresponse_parser 的来源:prompt 自己知道 怎么解析自己的回复。发什么和读什么在同一处声明,不会对不上。

4.3 分类模板 —— 判官的高层入口

src/evidently/llm/templates.py:23BaseLLMPromptTemplate 是判官模板基类(继承 BlockPromptTemplate),多定义了三个抽象方法让判官层能预知输出结构:list_output_columns (有哪些列)、get_type(每列什么类型)、get_main_output_column(主列是哪个)。

最常用的是 BinaryClassificationPromptTemplate(templates.py:95)。你声明"目标类 vs 非目标类 + 要不要 reasoning / score",它在 get_blocks()(templates.py:173)里把积木拼出来:

# 真实源码摘要 templates.py:184-193(二分类模板拼积木)
return [
PromptBlock.simple(self.criteria), # 你的评分标准
PromptBlock.simple(f"Classify text between {self.anchor_start} ..."),# 任务说明
PromptBlock.input().anchored(self.anchor_start, self.anchor_end), # 待评文本夹在锚点间
PromptBlock.simple(self._instructions()), # 类别定义 + 打分区间
PromptBlock.json_output(**fields), # 要求返回 JSON:哪几个字段
]

三个开关(include_category / include_score / include_reasoning,templates.py:123-128)决定 fields 里放哪几项,从而决定返回几列、prompt 里写不写"给个分数"list_output_columns (templates.py:198)和 get_type(templates.py:211)据此如实报告:category 是 Categorical、 score 是 Numerical、reasoning 是 Text——判官层就靠这个把 JSON 判决拆成正确类型的多列。

Uncertainty:处理"模型拿不准"。 判官不能被逼着二选一——上下文不足时硬分类会制造噪声。 Uncertainty 枚举(templates.py:80)给了三种策略:

策略含义
UNKNOWN单开一个 "UNKNOWN" 类别装拿不准的
TARGET拿不准就算目标类
NON_TARGET拿不准就算非目标类

_instructions(templates.py:142)会把选中的策略写进类别定义:"这个类别只在信息不足以下明确判断时用"。

多分类版 MulticlassClassificationPromptTemplate(templates.py:221)同理,只是类别来自 category_criteria 字典,且打分是每个类别一列(get_score_column,templates.py:291,列名 score_<类别>)。


5. 第三层:异步批量 LLM 引擎

前两层负责"发什么、怎么读",这一层负责"高效、可靠地把成百上千个请求发出去"。核心是 src/evidently/llm/utils/wrapper.py:228LLMWrapper 抽象基类。子类只需实现一个方法—— complete(messages)(wrapper.py:239,调具体 provider 的 API);并发、限流、重试、批处理 全由基类免费提供

5.1 数据结构:请求与结果

结构字段位置
LLMRequestmessages + response_parser + response_type + retrieswrapper.py:191
LLMResultresult(已解析) + input_tokens + output_tokenswrapper.py:205

请求携带解析器,结果携带 token 用量——限流器就靠 token 用量做反馈(见 §5.3)。

5.2 并发执行:run / run_batch / _batch

三个方法层层包装:

  • run(wrapper.py:304)—— 跑单个请求,内部 _run(wrapper.py:318)带重试:失败就重试 request.retries 次,全失败才抛最后一次的异常。
  • run_batch(wrapper.py:343)—— 跑一批,先给每个请求估算 token(用于限流),再交给 _batch
  • _batch(wrapper.py:252)—— 真正的并发核心:
# 真实源码摘要 wrapper.py:274-283(_batch 并发核心)
rate_limiter = RateLimiter(limits=limits)
semaphore = Semaphore(batch_size) # 并发上限,默认 100(get_batch_size)

async def work(request):
async with semaphore, rate_limiter.enter(request) as rate: # 双重闸门:并发数 + 速率
res = await coro(request.request)
rate.record(res.input_tokens, res.output_tokens) # 回填真实 token 给限流器学习
return res.result

return await asyncio.gather(*[work(batch) for batch in batches]) # 全部并发,等齐返回

两道闸门:Semaphore同时在飞的请求数(默认 100,wrapper.py:362),RateLimiter每分钟的请求/token 速率。跑完 rate.record 把真实 token 回填,让限流器越用越准。

判官层用的 run_batch_sync 是它的同步包装(wrapper.py:399,sync_api(run_batch))——所以判官的 generate_data 不必是 async 也能驱动整套异步并发。

5.3 按 token 估算限流:RateLimiter

RateLimits(wrapper.py:36)声明四种上限:rpm(每分钟请求数)、itpm/otpm/tpm (输入/输出/总 token 每分钟)。难点在于:请求发出前并不知道会用多少 token,尤其是输出。 RateLimiter(wrapper.py:144)的解法是估算 + 学习:

进入一个请求(_RateLimiterEntrypoint.__aenter__, wrapper.py:92):
循环: 拿锁 → 清理过期记录(clean) → 检查 RPM(_check_rpm) 和 token(_check_tokens)
都通过 → 记下这条 enter,放行
任一超限 → sleep(0.1) 再试
检查 token(_check_tokens, wrapper.py:123):
把当前窗口内每条记录的 token 加总——有真实值用真实值,没有就用估算值
估算的输出 token = mean_output_size():历史真实输出的均值(没历史就用初始值 10 万)

关键设计:输入 token 靠字符数估算(estimate_tokens,wrapper.py:386,默认 len(content)), 输出 token 靠历史均值估算(mean_output_size,wrapper.py:180)。请求跑完 record 回填真实值 (wrapper.py:115),stats 越积越多,输出估算越来越接近真实。这样即使 provider 只按 token 计费, 也能在发请求之前就把速率压在配额内。

5.4 多 Provider 适配:注册表 + LiteLLM

引擎要支持一堆 provider。机制是一张注册表 _wrappers(wrapper.py:405)+ 装饰器 @llm_provider(name, model)(wrapper.py:408)。查表逻辑 get_llm_wrapper(wrapper.py:416):

读法:从上到下依次尝试,命中即返回。

(provider, 具体model) 在注册表? ──是──▶ 用注册的 wrapper
│否
(provider, None) 在注册表? ──是──▶ 用该 provider 的通用 wrapper
│否
装了 litellm? ──是──▶ 用 LiteLLMWrapper 兜底
│否

抛错:找不到,提示装 litellm

只有两类"亲手写"的 wrapper:

  • OpenAIWrapper(wrapper.py:508,@llm_provider("openai", None))——直接调 openai SDK (complete,wrapper.py:539),按 loop 缓存 AsyncClient,把 RateLimitError/APIError 归一成 内部异常。默认限流 500 rpm(OpenAIKey,wrapper.py:496)。
  • LiteLLMWrapper(wrapper.py:575)——通过 LiteLLM 兜底几乎 所有 provider,complete(wrapper.py:594)调 acompletion

其余 provider(anthropic / gemini / deepseek / mistral / ollama / vertex_ai / nebius …)几乎都是 LiteLLMWrapper 的一行子类,只换一个 options 类携带各自的默认限流。例如 Anthropic (wrapper.py:619-628)默认限流按 5 秒窗口折算(rpm=50//12 等)。文件末尾更进一步:对一长串 litellm_providers 列表(wrapper.py:712)批量动态生成 wrapper+options 类 (_create_litellm_wrapper,wrapper.py:798;for provider in ... 循环 wrapper.py:829),自动注册进表。 所以新增一个 LiteLLM 支持的 provider,通常连代码都不用写。

API key 用 SecretStr 包裹(LLMOptions,wrapper.py:445),不会在日志/repr 里泄漏明文。


6. 巧妙之处(可借鉴的技术)

  • 请求自带解析器。 LLMRequest 把"发什么"和"回来怎么读"绑在一起(wrapper.py:191 + templates.py:110)。发送和解析在同一处声明,天然不会对不上——引擎完全不需要知道 prompt 长什么样。

  • 输出格式积木一物两用。 OutputFormatBlock(blocks.py:122)既渲染出"请返回 JSON"的指令、 又提供 parse_response。改输出结构只改一个积木,指令和解析同步变。

  • JSON 解析的"捞最大块"容错。 find_largest_json(blocks.py:139)用正则从模型啰嗦的回复里 抠出最大的合法 JSON,容忍模型在前后加解释——比"要求模型严格只输出 JSON"现实得多。

  • 限流器会自学习。 输出 token 事前不可知,RateLimiter 用历史真实均值估算、跑完回填 (wrapper.py:180/:115),估算随用随准,在超配额前就刹车。

  • provider 适配靠注册表 + 动态建类。 绝大多数 provider 是 LiteLLMWrapper 的一行子类,一整个 列表还能循环批量生成(wrapper.py:798/:829)。加 provider 近乎零代码。

  • docstring 即模板。 PromptBlock.render 拿类的 docstring 当渲染模板(blocks.py:34),积木的 "长相"就写在它的文档里,自解释。


7. 边界与局限

  • 判官本身是个 LLM,会错、会漂、会不稳。 同一输入不同次可能给不同判决;complete 支持 seed (wrapper.py:539)但不保证确定性。判官的"准不准"要靠人工标注去校准,这层代码不负责。

  • token 估算是粗的。 输入 token 默认按字符数算(estimate_tokens,wrapper.py:386),不是真 tokenizer;输出靠历史均值。限流是"够用的保守估计",不是精确配额管理。

  • 解析失败会重试后抛错。 _run(wrapper.py:318)重试 retries 次仍失败就抛异常;模型持续 不按格式回,这一行评估会失败。

  • _batch 一次 asyncio.gather 全量并发。 靠信号量(默认 100)和限流兜底;超大数据集下没有分片 流式处理,内存里会同时持有全部请求与结果。

  • 本章不含运行时拦截与 prompt 优化。 guardrails(生产时实时校验/拦截)和 llm/optimization (自动改进 prompt)属于观测/运维面,见 05-observability-and-ops.md


8. 代码地图(导航索引)

主题文件路径符号
自定义 prompt 判官src/evidently/descriptors/llm_judges.pyGenericLLMDescriptor
↳ 每行→请求src/evidently/descriptors/llm_judges.pyGenericLLMDescriptor.iterate_messages
↳ 批量跑+拆列src/evidently/descriptors/llm_judges.pyGenericLLMDescriptor.generate_data
模板驱动判官src/evidently/descriptors/llm_judges.pyLLMEvalLLMEval._judge
RAG 上下文相关性判官src/evidently/descriptors/_context_relevance.pyContextRelevance
↳ LLM 打分后端src/evidently/descriptors/_context_relevance.pyllm_scoring
↳ 分数聚合策略src/evidently/descriptors/_context_relevance.pyMeanAggregationHitAggregationHitShareAggregation
判官模板基类src/evidently/llm/templates.pyBaseLLMPromptTemplate
二分类模板src/evidently/llm/templates.pyBinaryClassificationPromptTemplateget_blocks
多分类模板src/evidently/llm/templates.pyMulticlassClassificationPromptTemplate
不确定性策略src/evidently/llm/templates.pyUncertainty
积木串模板src/evidently/llm/utils/templates.pyBlockPromptTemplatePromptTemplate.get_parser
Prompt 积木src/evidently/llm/utils/blocks.pyPromptBlockJsonOutputFormatBlockfind_largest_json
引擎基类src/evidently/llm/utils/wrapper.pyLLMWrappercomplete
并发/重试/批处理src/evidently/llm/utils/wrapper.pyrunrun_batch_batchrun_batch_sync
限流src/evidently/llm/utils/wrapper.pyRateLimiterRateLimits_check_tokensmean_output_size
请求/结果结构src/evidently/llm/utils/wrapper.pyLLMRequestLLMResult
provider 注册表src/evidently/llm/utils/wrapper.pyllm_providerget_llm_wrapper
provider 实现src/evidently/llm/utils/wrapper.pyOpenAIWrapperLiteLLMWrapper_create_litellm_wrapper
prompt 内容类型src/evidently/llm/prompts/content.pyPromptContentTemplatePromptContent