“The hottest new programming language is English.” —— Andrej Karpathy
在大语言模型时代,提示工程(Prompt Engineering) 已经成为一项关键技能。一个精心设计的 prompt 可以让模型的表现提升 10 倍,而糟糕的 prompt 可能让最强大的模型也无法发挥作用。
提示工程不仅仅是”问对问题”,它是一门结合了语言学、认知科学和机器学习的综合艺术。本文将系统性地介绍提示工程的核心原则、常用模式和高级技巧,帮助你成为”AI 时代的编译器优化专家”。
一、提示工程基础:从 Zero-shot 到 Few-shot
1.1 Zero-shot Prompting
定义:不提供任何示例,直接描述任务。
示例 1:文本分类
❌ 糟糕的 Prompt:
"这句话是什么情感?'这部电影太棒了!'"
✅ 优化后的 Prompt:
"你是一个情感分析专家。请判断以下句子的情感倾向(正面/负面/中性),并用一个词回答。
句子:'这部电影太棒了!'
情感:"
改进要点:
- 明确角色:设定模型的身份(专家、助手等)
- 清晰指令:说明具体任务和期望输出格式
- 结构化输出:引导模型生成格式化答案
示例 2:信息提取
prompt = """
从以下新闻中提取关键信息,以 JSON 格式输出:
新闻:
"特斯拉 CEO 埃隆·马斯克于 2024 年 10 月 15 日宣布,
公司将在德克萨斯州奥斯汀建立新的超级工厂,预计投资 50 亿美元。"
请提取:
{
"公司": "",
"CEO": "",
"事件": "",
"日期": "",
"地点": "",
"金额": ""
}
"""
1.2 Few-shot Prompting
核心思想:通过提供几个示例,让模型”学会”任务模式。
示例:情感分析(Few-shot)
你是一个情感分析专家。请判断句子的情感倾向。
示例:
句子:这家餐厅的服务很差。
情感:负面
句子:今天天气真不错!
情感:正面
句子:我买了一本书。
情感:中性
现在,请分析:
句子:这个产品性价比很高。
情感:
Few-shot 的黄金法则:
| 维度 | 建议 |
|---|---|
| 示例数量 | 2-5 个(更多不一定更好) |
| 示例质量 | 覆盖典型场景 + 边缘情况 |
| 示例顺序 | 从简单到复杂,最后一个最接近目标任务 |
| 示例多样性 | 避免重复模式,展示任务的多样性 |
高级技巧:动态示例选择
from sklearn.metrics.pairwise import cosine_similarity
import numpy as np
def select_few_shot_examples(query, example_pool, embeddings, k=3):
"""
根据查询语句,从示例池中选择最相似的 k 个示例
"""
query_embedding = get_embedding(query)
# 计算相似度
similarities = cosine_similarity([query_embedding], embeddings)[0]
# 选择最相似的 k 个
top_k_indices = np.argsort(similarities)[-k:][::-1]
return [example_pool[i] for i in top_k_indices]
# 使用
examples = select_few_shot_examples(
query="这款手机拍照效果很好",
example_pool=all_labeled_examples,
embeddings=all_embeddings,
k=3
)
二、Chain-of-Thought(CoT)推理
2.1 标准 CoT:让模型”展示思考过程”
问题:直接问答往往导致错误推理。
❌ 标准 Prompt:
"Roger 有 5 个网球。他又买了 2 罐网球,每罐 3 个球。他现在有多少个球?"
模型回答:"11 个。" (错误!)
✅ CoT Prompt:
Roger 有 5 个网球。他又买了 2 罐网球,每罐 3 个球。他现在有多少个球?
让我们一步步思考:
1. Roger 开始有 5 个球
2. 他买了 2 罐,每罐 3 个球
3. 2 罐 × 3 个/罐 = 6 个球
4. 总共:5 + 6 = 11 个球
答案:11 个球。
Few-shot CoT:提供带推理过程的示例
Q: 咖啡店里有 23 个苹果。如果他们用掉 20 个做午餐,又买了 6 个,现在有多少个?
A: 让我们一步步思考:
开始有 23 个苹果
用掉 20 个后:23 - 20 = 3 个
又买了 6 个:3 + 6 = 9 个
答案:9 个苹果
Q: Leah 有 32 颗巧克力,她姐姐有 42 颗。如果她们吃了 35 颗,还剩多少颗?
A: 让我们一步步思考:
Leah 和姐姐总共:32 + 42 = 74 颗
吃了 35 颗后:74 - 35 = 39 颗
答案:39 颗
Q: Jason 有 20 个棒棒糖。他给 Denny 一些后,还剩 12 个。他给了 Denny 多少个?
A:
2.2 Zero-shot CoT:魔法咒语
发现:只需添加”Let’s think step by step”,就能激活推理能力!
prompt = f"""
问题:{question}
Let's think step by step.
"""
# 中文版本
prompt = f"""
问题:{question}
让我们一步步思考:
"""
实验结果(GSM8K 数学数据集):
模型:GPT-3.5
┌─────────────────────────┬──────────┐
│ 方法 │ 准确率 │
├─────────────────────────┼──────────┤
│ 标准 Prompt │ 17.7% │
│ Zero-shot CoT │ 40.7% │
│ Few-shot CoT (8-shot) │ 46.9% │
│ Self-Consistency CoT │ 57.1% │
└─────────────────────────┴──────────┘
2.3 Self-Consistency:多次采样 + 投票
原理:让模型生成多个推理路径,选择最一致的答案。
def self_consistency_cot(question, model, num_samples=5):
prompt = f"{question}\n\nLet's think step by step."
# 生成多个推理路径
answers = []
for _ in range(num_samples):
response = model.generate(
prompt,
temperature=0.7, # 增加随机性
max_tokens=256
)
# 提取最终答案
answer = extract_answer(response)
answers.append(answer)
# 多数投票
from collections import Counter
most_common = Counter(answers).most_common(1)[0][0]
return most_common
# 使用
answer = self_consistency_cot(
"一个数字的两倍加 5 等于 13,这个数字是多少?",
model=gpt_model,
num_samples=5
)
2.4 CoT 变体
Least-to-Most Prompting:分解复杂问题
问题:Tom 有 5 个苹果,Mary 的苹果数是 Tom 的 3 倍,John 的苹果数是 Mary 的一半。
他们总共有多少个苹果?
让我们分解这个问题:
子问题 1:Mary 有多少个苹果?
- Tom 有 5 个
- Mary 是 Tom 的 3 倍:5 × 3 = 15 个
子问题 2:John 有多少个苹果?
- Mary 有 15 个
- John 是 Mary 的一半:15 ÷ 2 = 7.5 个(假设可以有半个)
最终问题:总共有多少个?
- Tom: 5 个
- Mary: 15 个
- John: 7.5 个
- 总计:5 + 15 + 7.5 = 27.5 个
Tree of Thoughts (ToT):探索多个推理分支
def tree_of_thoughts(problem, model, depth=3, breadth=3):
"""
在每一步生成多个可能的推理步骤,构建思维树
"""
def explore(state, depth_left):
if depth_left == 0:
return evaluate_solution(state)
# 生成 breadth 个下一步推理
next_steps = model.generate_next_thoughts(state, k=breadth)
# 递归探索每个分支
results = []
for step in next_steps:
new_state = state + [step]
score = explore(new_state, depth_left - 1)
results.append((score, new_state))
# 返回最佳分支
return max(results, key=lambda x: x[0])
return explore([], depth)
三、ReAct:推理 + 行动
3.1 核心概念
ReAct = Reasoning + Acting
让模型在推理过程中调用外部工具(搜索、计算器、数据库等)。
标准格式:
Thought: [模型的思考]
Action: [要执行的操作]
Action Input: [操作的输入]
Observation: [操作的结果]
... (重复 Thought/Action/Observation)
Thought: I now know the final answer
Final Answer: [最终答案]
3.2 实战示例
问题:埃隆·马斯克的年龄是多少?他的年龄的平方是多少?
Thought 1: 我需要先找到埃隆·马斯克的出生日期
Action 1: Search[埃隆·马斯克出生日期]
Observation 1: 埃隆·马斯克出生于 1971 年 6 月 28 日
Thought 2: 现在我需要计算他的年龄(当前是 2024 年)
Action 2: Calculate[2024 - 1971]
Observation 2: 53
Thought 3: 现在我需要计算 53 的平方
Action 3: Calculate[53 * 53]
Observation 3: 2809
Thought 4: 我现在知道最终答案了
Final Answer: 埃隆·马斯克 53 岁,他的年龄的平方是 2809。
3.3 代码实现
import re
from typing import List, Dict, Callable
class ReActAgent:
def __init__(self, model, tools: Dict[str, Callable], max_steps=5):
self.model = model
self.tools = tools
self.max_steps = max_steps
def run(self, question: str) -> str:
prompt = self._build_initial_prompt(question)
for step in range(self.max_steps):
# 生成下一步推理
response = self.model.generate(prompt)
# 解析 Thought 和 Action
thought = self._extract_thought(response)
action = self._extract_action(response)
if "Final Answer:" in response:
return self._extract_final_answer(response)
# 执行 Action
observation = self._execute_action(action)
# 更新 prompt
prompt += f"\nObservation {step+1}: {observation}\n"
return "达到最大步数限制"
def _execute_action(self, action: str) -> str:
# 解析 Action[tool_name, input]
match = re.search(r'(\w+)\[(.*?)\]', action)
if match:
tool_name, tool_input = match.groups()
if tool_name in self.tools:
return self.tools[tool_name](tool_input)
return "无效的 Action"
def _build_initial_prompt(self, question: str) -> str:
tools_desc = "\n".join([
f"- {name}: {func.__doc__}"
for name, func in self.tools.items()
])
return f"""
Answer the following question using this format:
Thought: [your reasoning]
Action: [tool_name[input]]
Observation: [result will be provided]
... (repeat Thought/Action/Observation)
Thought: I now know the final answer
Final Answer: [your final answer]
Available tools:
{tools_desc}
Question: {question}
Thought 1:"""
# 定义工具
def search(query: str) -> str:
"""Search the web for information"""
# 实际实现中调用搜索 API
return f"Search results for '{query}': ..."
def calculate(expression: str) -> str:
"""Evaluate a mathematical expression"""
try:
return str(eval(expression))
except:
return "Invalid expression"
def lookup(keyword: str) -> str:
"""Look up information in a knowledge base"""
# 实际实现中查询数据库
return f"Information about '{keyword}': ..."
# 使用
agent = ReActAgent(
model=gpt_model,
tools={
"Search": search,
"Calculate": calculate,
"Lookup": lookup
}
)
answer = agent.run("埃隆·马斯克的年龄的平方是多少?")
3.4 ReAct 的优势
| 特性 | 纯推理(CoT) | ReAct |
|---|---|---|
| 知识时效性 | ❌ 受限于训练数据 | ✅ 实时获取最新信息 |
| 精确计算 | ❌ 容易出错 | ✅ 调用计算器工具 |
| 可解释性 | ⭐⭐⭐ | ⭐⭐⭐⭐⭐ |
| 复杂度 | 简单 | 中等 |
四、高级提示工程技巧
4.1 角色设定(Role Prompting)
原理:通过设定明确的角色,激活模型的特定”人格”。
# 基础版本
prompt = "解释量子力学"
# 角色增强版本
prompt = """
你是一位获得诺贝尔物理学奖的量子力学专家,擅长用通俗易懂的语言
向非专业人士解释复杂概念。请用初中生能理解的方式解释量子力学。
"""
# 多角色对话
prompt = """
你将扮演三个角色进行对话:
1. 学生(好奇但缺乏背景知识)
2. 老师(耐心且善于类比)
3. 科学家(严谨且注重细节)
请围绕"什么是黑洞"展开一场对话。
"""
4.2 约束与格式化
技巧 1:输出格式约束
prompt = f"""
请分析以下产品评论的情感,并严格按照 JSON 格式输出:
评论:"{review}"
输出格式(必须严格遵守):
{{
"sentiment": "positive/negative/neutral",
"confidence": 0.0-1.0,
"key_phrases": ["phrase1", "phrase2"],
"summary": "一句话总结"
}}
不要输出除 JSON 之外的任何内容。
"""
技巧 2:长度约束
# 方法 1:明确指定
prompt = "用不超过 50 个字总结这篇文章"
# 方法 2:使用模板
prompt = """
请总结以下文章。
文章:{article}
总结(50字以内):
"""
# 方法 3:Token 限制
response = model.generate(prompt, max_tokens=100)
技巧 3:风格约束
style_templates = {
"formal": "请用正式、学术的语气回答",
"casual": "请用轻松、口语化的方式回答",
"technical": "请用专业术语和技术细节回答",
"eli5": "请用向 5 岁小孩解释的方式回答"
}
prompt = f"""
{style_templates["eli5"]}
问题:什么是区块链?
"""
4.3 分步骤引导(Step-by-Step Guidance)
prompt = """
请帮我写一篇关于人工智能的文章。按照以下步骤进行:
步骤 1:列出文章的 5 个主要章节标题
步骤 2:为每个章节写 3-5 个要点
步骤 3:为第一章节写一个详细的段落(200字)
步骤 4:总结全文的核心观点(50字)
现在开始步骤 1:
"""
4.4 对比学习(Contrastive Prompting)
prompt = """
以下是两种写作风格的对比。请学习"好的风格",避免"差的风格"。
❌ 差的风格(冗长、模糊):
"这个产品在某种程度上可能会对用户的使用体验产生一定的影响,
具体来说可能涉及到多个方面的因素..."
✅ 好的风格(简洁、明确):
"这个产品有三个缺点:1)启动慢 2)界面复杂 3)价格偏高"
现在,请用"好的风格"重写以下段落:
"{text}"
"""
4.5 元提示(Meta-Prompting)
让模型生成提示:
meta_prompt = """
我想要一个 prompt,能让 AI 模型生成高质量的产品描述。
这个 prompt 应该:
1. 包含产品的核心特性
2. 突出卖点
3. 引发情感共鸣
4. 控制在 100 字以内
请帮我设计这个 prompt。
"""
# 模型生成的 prompt:
generated_prompt = """
请为以下产品撰写吸引人的描述:
产品名称:{name}
核心特性:{features}
目标用户:{audience}
要求:
- 开头用一个引人注目的问题或场景
- 突出 2-3 个最重要的卖点
- 用感性语言描述使用体验
- 以行动号召结尾
- 总字数 80-100 字
"""
五、提示优化策略
5.1 迭代优化流程
class PromptOptimizer:
def __init__(self, model, eval_dataset):
self.model = model
self.eval_dataset = eval_dataset
def optimize(self, initial_prompt, num_iterations=5):
current_prompt = initial_prompt
best_score = 0
for i in range(num_iterations):
# 1. 评估当前 prompt
score = self.evaluate(current_prompt)
print(f"Iteration {i+1}: Score = {score:.2f}")
if score > best_score:
best_score = score
best_prompt = current_prompt
# 2. 分析失败案例
failures = self.get_failure_cases(current_prompt)
# 3. 生成改进建议
suggestions = self.generate_suggestions(
current_prompt, failures
)
# 4. 修改 prompt
current_prompt = self.apply_suggestions(
current_prompt, suggestions
)
return best_prompt, best_score
def evaluate(self, prompt):
"""在评估集上测试 prompt"""
correct = 0
for example in self.eval_dataset:
response = self.model.generate(
prompt.format(**example)
)
if self.is_correct(response, example['answer']):
correct += 1
return correct / len(self.eval_dataset)
5.2 A/B 测试框架
import pandas as pd
from scipy import stats
class PromptABTest:
def __init__(self, model, test_cases):
self.model = model
self.test_cases = test_cases
def compare(self, prompt_a, prompt_b, metric='accuracy'):
results_a = []
results_b = []
for case in self.test_cases:
# 测试 Prompt A
response_a = self.model.generate(prompt_a.format(**case))
score_a = self.evaluate_response(response_a, case)
results_a.append(score_a)
# 测试 Prompt B
response_b = self.model.generate(prompt_b.format(**case))
score_b = self.evaluate_response(response_b, case)
results_b.append(score_b)
# 统计显著性检验
t_stat, p_value = stats.ttest_rel(results_a, results_b)
report = {
'prompt_a_mean': np.mean(results_a),
'prompt_b_mean': np.mean(results_b),
'improvement': np.mean(results_b) - np.mean(results_a),
'p_value': p_value,
'significant': p_value < 0.05
}
return report
# 使用
tester = PromptABTest(model, test_cases)
result = tester.compare(
prompt_a="Translate to French: {text}",
prompt_b="You are a professional translator. Translate to French: {text}"
)
print(f"Improvement: {result['improvement']:.2%}")
print(f"Significant: {result['significant']}")
5.3 自动化提示工程工具
Prompt Flow:
from promptflow import PromptTemplate, Optimizer
# 定义模板(带占位符)
template = PromptTemplate("""
{role_description}
Task: {task_description}
{few_shot_examples}
Input: {input}
Output:
""")
# 自动搜索最佳配置
optimizer = Optimizer(
template=template,
model=model,
eval_dataset=eval_data,
search_space={
'role_description': [
'You are an expert.',
'You are a helpful assistant.',
'You are a domain specialist.'
],
'task_description': [
'Classify the sentiment.',
'Analyze the sentiment and explain.',
],
'few_shot_examples': [0, 2, 4] # 示例数量
}
)
best_prompt = optimizer.optimize(
metric='f1_score',
num_trials=50
)
六、领域特定模式
6.1 代码生成
code_prompt = """
你是一位资深的 Python 工程师。请编写高质量、可维护的代码。
任务:{task_description}
要求:
1. 使用类型提示(type hints)
2. 添加 docstring 文档
3. 包含错误处理
4. 编写单元测试
5. 遵循 PEP 8 规范
示例格式:
```python
def function_name(param: Type) -> ReturnType:
\"\"\"
Function description.
Args:
param: Parameter description
Returns:
Return value description
Raises:
ExceptionType: When this exception occurs
\"\"\"
# Implementation
pass
现在开始: “””
### 6.2 数据分析
```python
analysis_prompt = """
你是一位数据分析专家。请对以下数据进行分析:
数据:
{data}
分析要求:
1. 描述性统计(均值、中位数、标准差)
2. 识别异常值
3. 发现趋势和模式
4. 提出业务洞察
5. 给出可视化建议
请按照以下格式输出:
## 数据概览
[统计摘要]
## 关键发现
- 发现 1:[具体数据支撑]
- 发现 2:[具体数据支撑]
## 业务建议
[可执行的建议]
## 可视化方案
[推荐的图表类型及原因]
"""
6.3 创意写作
creative_prompt = """
你是一位获得雨果奖的科幻小说作家。
写作任务:创作一个短篇故事开头
故事设定:
- 时间:{time_period}
- 地点:{location}
- 主角:{protagonist}
- 冲突:{conflict}
写作风格:
- 开门见山,直接进入场景
- 使用感官细节(视觉、听觉、触觉)
- 制造悬念,引发好奇
- 展示而非告知(show, don't tell)
字数:300-500 字
现在开始写作:
"""
七、常见陷阱与解决方案
7.1 问题:模型”幻觉”(生成虚假信息)
解决方案 1:要求引用来源
prompt = """
请回答以下问题,并为每个关键事实标注来源。
如果不确定,请明确说明"我不确定"。
问题:{question}
回答格式:
[主要回答]
来源:
1. [事实 1] - 来源:[...]
2. [事实 2] - 来源:[...]
"""
解决方案 2:检索增强生成(RAG)
def rag_prompt(question, retrieved_docs):
context = "\n\n".join([
f"文档 {i+1}:{doc}"
for i, doc in enumerate(retrieved_docs)
])
return f"""
请基于以下文档回答问题。只使用文档中的信息,不要添加额外内容。
如果文档中没有答案,请说"文档中未找到相关信息"。
文档:
{context}
问题:{question}
回答(必须基于文档):
"""
7.2 问题:输出格式不一致
解决方案:结构化输出 + 后处理
import json
import re
def enforce_json_output(prompt, model, max_retries=3):
"""确保模型输出有效的 JSON"""
for attempt in range(max_retries):
response = model.generate(prompt)
# 尝试提取 JSON
json_match = re.search(r'\{.*\}', response, re.DOTALL)
if json_match:
try:
return json.loads(json_match.group())
except json.JSONDecodeError:
pass
# 如果失败,修改 prompt 重试
prompt += "\n\n请确保输出是有效的 JSON 格式,不要包含任何其他文本。"
raise ValueError("无法获取有效的 JSON 输出")
7.3 问题:上下文长度限制
解决方案:智能截断与分块
def smart_truncation(text, max_tokens, model):
"""智能截断,保留最重要的部分"""
# 方法 1:截取首尾
tokens = model.tokenize(text)
if len(tokens) <= max_tokens:
return text
# 保留 70% 开头 + 30% 结尾
head_len = int(max_tokens * 0.7)
tail_len = max_tokens - head_len
truncated_tokens = tokens[:head_len] + tokens[-tail_len:]
return model.detokenize(truncated_tokens)
# 方法 2:分块处理 + 汇总
def process_long_text(text, task, model):
chunks = split_into_chunks(text, max_tokens=2000)
# 对每个块处理
chunk_results = []
for chunk in chunks:
result = model.generate(f"{task}\n\nText: {chunk}")
chunk_results.append(result)
# 汇总结果
final_prompt = f"""
以下是对文档各部分的分析结果:
{chr(10).join(chunk_results)}
请综合以上分析,给出整体结论:
"""
return model.generate(final_prompt)
八、提示工程工具与资源
8.1 开源工具
LangChain:
from langchain import PromptTemplate, LLMChain
from langchain.llms import OpenAI
# 定义模板
template = PromptTemplate(
input_variables=["product", "audience"],
template="""
为 {product} 写一段营销文案,目标受众是 {audience}。
文案应该:
- 突出核心价值
- 引发情感共鸣
- 包含行动号召
文案:
"""
)
# 创建链
chain = LLMChain(llm=OpenAI(), prompt=template)
# 使用
result = chain.run(product="智能手表", audience="运动爱好者")
Guidance:
from guidance import models, gen
# 定义结构化生成
gpt = models.OpenAI("gpt-3.5-turbo")
result = gpt + f"""
请分析以下评论的情感:
评论:{review}
情感分析:
- 情感倾向:{gen('sentiment', regex='(positive|negative|neutral)')}
- 置信度:{gen('confidence', regex='[0-9]\\.[0-9]+')}
- 关键词:{gen('keywords', list_append=True, n=3)}
"""
8.2 Prompt 库
Awesome Prompts:
- GitHub 仓库
- 包含各种场景的高质量 prompt 模板
OpenAI Cookbook:
- 官方最佳实践
- 涵盖各种用例的代码示例
8.3 评估工具
PromptBench:
from promptbench import PromptEvaluator
evaluator = PromptEvaluator(
model="gpt-3.5-turbo",
dataset="sst2", # 情感分析数据集
metrics=["accuracy", "f1", "consistency"]
)
results = evaluator.evaluate([
"Classify sentiment: {text}",
"What is the sentiment? {text}",
"Analyze: {text} Sentiment:"
])
print(evaluator.rank_prompts())
九、未来展望
9.1 自动化提示优化
APE(Automatic Prompt Engineer):
- 让 LLM 自动生成和优化 prompt
- 通过进化算法迭代改进
9.2 多模态提示
# 图像 + 文本提示
prompt = {
"image": "path/to/image.jpg",
"text": """
请描述这张图片中的场景,并回答:
1. 图片的主题是什么?
2. 有哪些值得注意的细节?
3. 这张图片可能用于什么场景?
"""
}
9.3 个性化提示
根据用户历史交互,动态调整 prompt 风格和复杂度。
十、总结
提示工程的核心原则:
- 清晰明确:告诉模型”做什么”而非”不做什么”
- 提供上下文:角色、任务、期望输出格式
- 分步骤引导:复杂任务分解为小步骤
- 使用示例:Few-shot 学习提供参考
- 迭代优化:持续测试和改进
实践建议:
- 从简单 prompt 开始,逐步增加复杂度
- 建立自己的 prompt 模板库
- 使用 A/B 测试验证改进效果
- 关注输出一致性和可靠性
- 结合工具(RAG、Code Interpreter)提升能力
记住:提示工程是一门实践科学,没有”完美”的 prompt,只有”更好”的 prompt。持续实验、评估和优化是成功的关键!
参考资源
论文:
- Chain-of-Thought Prompting (2022)
- ReAct: Synergizing Reasoning and Acting (2022)
- Large Language Models are Zero-Shot Reasoners (2022)
工具与框架:
- LangChain
- Guidance
- PromptBase - Prompt 市场
学习资源:
下一篇文章将探讨 RAG 检索增强生成实战,敬请期待!
💬 交流与讨论
⚠️ 尚未完成 Giscus 配置。请在
_config.yml中设置repo_id与category_id后重新部署,即可启用升级后的评论系统。配置完成后,评论区将自动支持 Markdown 代码高亮与 LaTeX 数学公式渲染,访客回复会同步到 GitHub Discussions,并具备通知功能。