
前几天我想用一个之前调了很久的prompt,让AI帮我写SQL查询。我记得当时调了快一个小时,效果特别好,准确率很高。
但我存在哪了?
先翻了ChatGPT的聊天记录,翻了二十分钟没找到。又翻了飞书文档,有个文档叫“prompt备份_final_v2”,打开一看是空的。再翻备忘录,有一条记着“SQL prompt见电脑桌面txt”,桌面上倒是有个txt,但内容只有半截,后面被截断了。
找了快一个小时,最后在一个叫“新建文本文档 (3).txt”的文件里找到了完整版。你说气不气。
这件事让我意识到,prompt这东西需要好好管一下。它不像代码有Git管理,也不像文档有Notion。好的prompt散落在聊天记录、便签、文档、txt文件里,过几天就找不着了。
当天晚上我就写了个Prompt管理工具。SQLite存数据,Jinja2做模板渲染,命令行操作。功能不复杂,但够用:增删改查、版本管理、变量替换、效果评分。
# pip install jinja2 rich
import sqlite3
import json
from datetime import datetime
from jinja2 import Template
from rich.console import Console
from rich.table import Table
console = Console()
DB_PATH = "prompts.db"
# ============================================================
# 数据库初始化
# ============================================================
def init_db():
"""建表,如果不存在的话"""
conn = sqlite3.connect(DB_PATH)
c = conn.cursor()
# prompt主表
c.execute("""
CREATE TABLE IF NOT EXISTS prompts (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT UNIQUE NOT NULL,
category TEXT DEFAULT 'general',
description TEXT,
created_at TEXT,
updated_at TEXT
)
""")
# 版本表,一个prompt可以有多个版本
c.execute("""
CREATE TABLE IF NOT EXISTS versions (
id INTEGER PRIMARY KEY AUTOINCREMENT,
prompt_id INTEGER,
version INTEGER,
template TEXT NOT NULL,
variables TEXT,
score REAL DEFAULT 0,
notes TEXT,
created_at TEXT,
FOREIGN KEY (prompt_id) REFERENCES prompts(id)
)
""")
conn.commit()
conn.close()
# ===================================================
# CRUD 操作
# ===================================================
def add_prompt(name, template, category="general", description="", variables=None):
"""添加一个新的prompt"""
conn = sqlite3.connect(DB_PATH)
c = conn.cursor()
now = datetime.now().isoformat()
try:
c.execute(
"INSERT INTO prompts (name, category, description, created_at, updated_at) VALUES (?, ?, ?, ?, ?)",
(name, category, description, now, now)
)
prompt_id = c.lastrowid
# 自动创建第一个版本
c.execute(
"INSERT INTO versions (prompt_id, version, template, variables, created_at) VALUES (?, ?, ?, ?, ?)",
(prompt_id, 1, template, json.dumps(variablesor []), now)
)
conn.commit()
console.print(f"[green]添加成功: {name} (版本1)[/green]")
return prompt_id
except sqlite3.IntegrityError:
console.print(f"[red]已存在同名prompt: {name}[/red]")
return None
finally:
conn.close()
def update_prompt(name, template, notes=""):
"""更新prompt,自动创建新版本"""
conn = sqlite3.connect(DB_PATH)
c = conn.cursor()
now = datetime.now().isoformat()
# 找到这个prompt
c.execute("SELECT id FROM prompts WHERE name = ?", (name,))
row = c.fetchone()
if not row:
console.print(f"[red]找不到prompt: {name}[/red]")
conn.close()
return
prompt_id = row[0]
# 查当前最新版本号
c.execute("SELECT MAX(version) FROM versions WHERE prompt_id = ?", (prompt_id,))
max_version = c.fetchone()[0] or0
new_version = max_version+1
# 提取模板里的变量名
variables = Template(template).environment.parse(template).find_all(lambdan: hasattr(n, 'name'))
var_names = list(set(
node.namefornodeinTemplate(template).environment.parse(template).find_all(lambdan: hasattr(n, 'name'))
ifhasattr(node, 'name')
))
# 简单方式:用正则找变量
import re
var_names = re.findall(r'\{\{\s*(\w+)\s*\}\}', template)
c.execute(
"INSERT INTO versions (prompt_id, version, template, variables, notes, created_at) VALUES (?, ?, ?, ?, ?, ?)",
(prompt_id, new_version, template, json.dumps(var_names), notes, now)
)
# 更新时间
c.execute("UPDATE prompts SET updated_at = ? WHERE id = ?", (now, prompt_id))
conn.commit()
conn.close()
console.print(f"[green]已更新: {name} (版本{new_version})[/green]")
def delete_prompt(name):
"""删除prompt和它的所有版本"""
conn = sqlite3.connect(DB_PATH)
c = conn.cursor()
c.execute("SELECT id FROM prompts WHERE name = ?", (name,))
row = c.fetchone()
if not row:
console.print(f"[red]找不到prompt: {name}[/red]")
conn.close()
return
prompt_id = row[0]
c.execute("DELETE FROM versions WHERE prompt_id = ?", (prompt_id,))
c.execute("DELETE FROM prompts WHERE id = ?", (prompt_id,))
conn.commit()
conn.close()
console.print(f"[yellow]已删除: {name}[/yellow]")
def list_prompts(category=None):
"""列出所有prompt"""
conn = sqlite3.connect(DB_PATH)
c = conn.cursor()
if category:
c.execute("SELECT * FROM prompts WHERE category = ? ORDER BY updated_at DESC", (category,))
else:
c.execute("SELECT * FROM prompts ORDER BY updated_at DESC")
rows = c.fetchall()
conn.close()
if not rows:
console.print("[dim]还没有保存任何prompt[/dim]")
return
table = Table(title="已保存的Prompt")
table.add_column("名称", style="cyan")
table.add_column("分类", style="green")
table.add_column("说明")
table.add_column("更新时间")
for row in rows:
_, name, cat, desc, created, updated = row
table.add_row(name, cat, descor"", updated[:16])
console.print(table)
# ===================================================
# 模板渲染
# ===================================================
def render_prompt(name, version=None, **kwargs):
"""渲染prompt模板,替换变量"""
conn = sqlite3.connect(DB_PATH)
c = conn.cursor()
# 找到prompt
c.execute("SELECT id FROM prompts WHERE name = ?", (name,))
row = c.fetchone()
if not row:
conn.close()
returnNone
prompt_id = row[0]
# 拿指定版本,或者最新的
if version:
c.execute(
"SELECT template FROM versions WHERE prompt_id = ? AND version = ?",
(prompt_id, version)
)
else:
c.execute(
"SELECT template FROM versions WHERE prompt_id = ? ORDER BY version DESC LIMIT 1",
(prompt_id,)
)
row = c.fetchone()
conn.close()
if not row:
console.print(f"[red]找不到对应版本[/red]")
returnNone
template_str = row[0]
# 用Jinja2渲染
tmpl = Template(template_str)
rendered = tmpl.render(**kwargs)
return rendered
# ===================================================
# 评分功能
# ===================================================
def rate_prompt(name, version, score, notes=""):
"""给某个版本打分"""
conn = sqlite3.connect(DB_PATH)
c = conn.cursor()
c.execute("SELECT id FROM prompts WHERE name = ?", (name,))
row = c.fetchone()
if not row:
conn.close()
return
prompt_id = row[0]
c.execute(
"UPDATE versions SET score = ?, notes = ? WHERE prompt_id = ? AND version = ?",
(score, notes, prompt_id, version)
)
conn.commit()
conn.close()
console.print(f"[green]已打分: {name} v{version} = {score}分[/green]")
def show_versions(name):
"""显示一个prompt的所有版本"""
conn = sqlite3.connect(DB_PATH)
c = conn.cursor()
c.execute("SELECT id FROM prompts WHERE name = ?", (name,))
row = c.fetchone()
if not row:
conn.close()
return
prompt_id = row[0]
c.execute(
"SELECT version, template, variables, score, notes, created_at FROM versions WHERE prompt_id = ? ORDER BY version DESC",
(prompt_id,)
)
rows = c.fetchall()
conn.close()
table = Table(title=f"{name} 版本历史")
table.add_column("版本", style="cyan")
table.add_column("得分", style="green")
table.add_column("备注")
table.add_column("模板预览")
table.add_column("创建时间")
for row in rows:
ver, tmpl, vars_, score, notes, created = row
preview = tmpl[:60] +"..."if len(tmpl) >60 else tmpl
score_str = str(score) ifscoreelse"-"
table.add_row(f"v{ver}", score_str, notesor"", preview, created[:16])
console.print(table)
# ===================================================
# 交互式命令行
# ===================================================
def interactive_mode():
"""交互式CLI,可以输入命令操作"""
init_db()
console.print("\n[bold cyan]Prompt管理工具[/bold cyan]")
console.print("命令: add | list | update | delete | render | versions | rate | quit\n")
while True:
cmd = console.input("[bold]>>>[/bold] ").strip()
ifcmd == "quit"orcmd == "q":
console.print("拜拜。")
break
elif cmd == "list":
cat = console.input("分类筛选(回车跳过): ").strip() orNone
list_prompts(cat)
elif cmd == "add":
name = console.input("名称: ").strip()
category = console.input("分类: ").strip() or"general"
description = console.input("说明: ").strip()
console.print("模板(输入END结束):")
lines = []
whileTrue:
line = input()
if line.strip() == "END":
break
lines.append(line)
template = "\n".join(lines)
add_prompt(name, template, category, description)
elif cmd == "render":
name = console.input("Prompt名称: ").strip()
ver = console.input("版本号(回车用最新): ").strip()
ver = int(ver) ifverelseNone
# 看看需要什么变量
result = render_prompt(name, ver)
if result:
console.print("\n[bold]渲染结果:[/bold]")
console.print(result)
elif cmd == "versions":
name = console.input("Prompt名称: ").strip()
show_versions(name)
elif cmd == "rate":
name = console.input("Prompt名称: ").strip()
ver = int(console.input("版本号: ").strip())
score = float(console.input("得分(1-10): ").strip())
notes = console.input("备注: ").strip()
rate_prompt(name, ver, score, notes)
elif cmd == "delete":
name = console.input("要删除的名称: ").strip()
confirm = console.input(f"确认删除 {name}?(y/n): ").strip()
if confirm == "y":
delete_prompt(name)
else:
console.print("[dim]不认识的命令,试试: add list update delete render versions rate[/dim]")
if __name__ == "__main__":
interactive_mode()我先添加了几个常用的prompt,然后试了一下渲染功能。
Prompt管理工具
命令: add | list | update | delete | render | versions | rate | quit
>>> add
名称: sql_query
分类: coding
说明: 让AI根据表结构写SQL查询
模板(输入END结束):
你是一个SQL专家。根据以下表结构写SQL查询。
表名: {{table_name}}
字段: {{columns}}
要求:
- 只写SQL,不要解释
- 考虑性能,用合适的索引建议
- 如果涉及日期,用 {{date_format}} 格式
用户问题: {{question}}
END
添加成功: sql_query (版本1)
>>> list
┌──────────────────────────────────────────────────────────────┐
│ 已保存的Prompt │
├──────────┬────────┬──────────────────────┬──────────────────┤
│ 名称 │ 分类 │ 说明 │ 更新时间 │
├──────────┼────────┼──────────────────────┼──────────────────┤
│ sql_query│ coding │ 让AI根据表结构写SQL │ 2024-12-15T14:30 │
│ code_rev │ coding │ 代码审查prompt │ 2024-12-14T09:15 │
│ email │ work │ 写工作邮件 │ 2024-12-13T16:20 │
└──────────┴────────┴──────────────────────┴──────────────────┘
>>> render
Prompt名称: sql_query
版本号(回车用最新):
渲染结果:
你是一个SQL专家。根据以下表结构写SQL查询。
表名: orders
字段: id, user_id, amount, status, created_at
要求:
- 只写SQL,不要解释
- 考虑性能,用合适的索引建议
- 如果涉及日期,用 %Y-%m-%d 格式
用户问题: 上个月消费超过1000元的用户有哪些渲染功能用Jinja2实现的,{{变量名}}会被替换成你传入的值。这个很方便,同一个prompt模板,传不同的参数就能用在不同场景。
为什么要存SQLite。 一开始我想用JSON文件存,但考虑到以后prompt会越来越多,查询、排序、筛选用数据库方便。SQLite不用装额外服务,一个文件搞定,对这个场景刚好够用。
版本管理很重要。 一个prompt经常要改好几次才能调到最佳效果。每次改的时候自动创建一个新版本,旧版本不丢。万一改坏了还能回退。每个版本还可以打分,时间长了你就知道哪个版本效果最好。
Jinja2比format好用。 Python自带的str.format()也能做变量替换,但Jinja2支持条件判断、循环、过滤器这些高级功能。比如你可以在模板里写{% if include_examples %}这里是示例...{% endif %},按需包含内容。
rich库让命令行好看很多。 用rich的Table和Console,命令行输出的表格是彩色的,对齐也整齐。比print一堆纯文本看着舒服多了。
我把这个工具放在电脑上常驻了一个快捷方式。每次调到一个好用的prompt,直接add进去。用的时候render一下,复制粘贴。
两个月下来,存了40多个prompt。代码审查的、写周报的、生成测试用例的、翻译技术文档的,各种类型都有。每次不用再从头调了,省了不少时间。
同事看我用得顺手,让我也给他装了一份。现在我们俩共享一个prompts.db文件,谁调出了好用的prompt就存进去,另一个人直接拿来用。
本文分享自 Nicholas与Pypi 微信公众号,前往查看
如有侵权,请联系 cloudcommunity@tencent.com 删除。
本文参与 腾讯云自媒体同步曝光计划 ,欢迎热爱写作的你一起参与!