上一篇文章提到,我将网站的说说功能从原本与网站本体强耦合的本地文件管理方式,迁移到了远程拉取加载的架构。这个改变看似简单,实则解决了困扰我已久的技术债务——每次新增或修改说说,都会在 Git 仓库中留下一条 Commit 记录,这些记录混杂在网站功能迭代中,导致提交历史变得臃肿且难以追溯。远程拉取方案将数据独立存放,网站只需在访问时用 JS 请求 JSON 文件即可,Git 日志恢复了清爽。

正当我准备收工时,脑海中突然闪过一个被我遗忘的角落:很久以前我写过一个纯 Shell 脚本专门处理说说,但那个脚本只支持 YAML 格式,而新方案用的是 JSON。既然已经决定重构,那就干脆做彻底一点——不仅仅是换脚本语言,连说说的标签系统也要重新设计。这些标签和网站文章的标签完全是两回事,文章标签用于 SEO 和分类检索,而说说标签更像社交媒体上的标签。

我找到了之前写过的 auto-tags.py——那是一个为文章自动生成标签的脚本,逻辑还算完整。既然有现成的轮子,何不拿来改造一下?于是我开始动手,新建了一个 auto-essay-tags.py。这个脚本的核心思路和我之前写的代码完全不同:原有的 auto-tags.py 会先判断文章是否已有标签、避免重复覆盖,但说说场景下不需要这么小心翼翼。

auto-essay-tags.py 是一个彻头彻尾的一次性脚本,它的任务简单粗暴——遍历整个 essay.json 文件中的每一条说说,调用无状态的大语言模型 API,让 AI 根据内容自动生成 3 个合适的社交媒体风格标签。脚本会读取共用标签库 essay-tags.md,提示 AI 优先复用已有标签,只有当确实没有合适选项时才创造新词:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
import sys
import os
import json
import time
import urllib.request
import urllib.error

API_URL = "https://api.deepseek.com/chat/completions"
API_KEY = ""
MODEL = "deepseek-v4-flash"
DELAY = 0.5

PROMPT_TEMPLATE = """请阅读以下动态内容,为这条动态生成 3 个社交媒体风格的标签。
要求:
1. 标签要像社交媒体的 hashtag,简洁、有话题感,适合日常分享
2. 只输出标签,用中文逗号分隔,不要输出其他任何内容
3. 格式示例:日常,美食,生活感悟
{existing_tags_section}
动态内容:
"""


def read_file(path):
with open(path, "r", encoding="utf-8") as f:
return f.read()


def write_file(path, content):
with open(path, "w", encoding="utf-8") as f:
f.write(content)


def build_prompt(essay_content, existing_tags):
if existing_tags:
tags_str = "、".join(sorted(existing_tags))
section = "4. 以下是本站已有的标签列表,请优先从中选择合适的标签复用,只有当已有标签无法概括时才生成新标签:\n" + tags_str
else:
section = ""
return PROMPT_TEMPLATE.format(existing_tags_section=section) + essay_content


def call_api(essay_content, existing_tags):
headers = {
"Content-Type": "application/json",
"Authorization": "Bearer " + API_KEY,
}
payload = {
"model": MODEL,
"messages": [{"role": "user", "content": build_prompt(essay_content, existing_tags)}],
"temperature": 0.3,
}
body = json.dumps(payload).encode("utf-8")
req = urllib.request.Request(API_URL, data=body, headers=headers, method="POST")
try:
with urllib.request.urlopen(req) as resp:
result = json.loads(resp.read().decode("utf-8"))
return result["choices"][0]["message"]["content"].strip()
except urllib.error.HTTPError as e:
print("API HTTP error: {} {}".format(e.code, e.reason))
return None
except urllib.error.URLError as e:
print("API request failed: {}".format(e.reason))
return None


def parse_tags(response):
if not response:
return []
for sep in [",", ","]:
if sep in response:
tags = [t.strip().strip('"').strip("'").strip("#") for t in response.split(sep)]
tags = [t for t in tags if t]
return tags[:3]
return [response.strip().strip('"').strip("'").strip("#")][:3]


def load_existing_tags(tags_file):
if not os.path.exists(tags_file):
return set()
content = read_file(tags_file).strip()
if not content:
return set()
return set(line.strip() for line in content.split("\n") if line.strip())


def save_tags(tags_file, all_tags):
write_file(tags_file, "\n".join(sorted(all_tags)) + "\n")


def main():
if len(sys.argv) < 2:
print("Usage: python essay-auto-tags.py <essay.json_path>")
sys.exit(1)

json_path = os.path.abspath(sys.argv[1])
if not os.path.exists(json_path):
print("File not found: " + json_path)
sys.exit(1)

script_dir = os.path.dirname(os.path.abspath(__file__))
tags_file = os.path.join(script_dir, "essay-tags.md")

existing_tags = load_existing_tags(tags_file)

essays = json.loads(read_file(json_path))

print("Total entries: " + str(len(essays)))
print("Existing tags count: " + str(len(existing_tags)))

all_tags = set(existing_tags)
failed = 0

for i, essay in enumerate(essays):
content = essay.get("content", "")
key = essay.get("key", str(i))
if not content:
print("[{}/{}] Skipping empty entry (key={})".format(i + 1, len(essays), key))
continue

print("[{}/{}] Processing key={} ...".format(i + 1, len(essays), key))
response = call_api(content, existing_tags)
if response is None:
print(" API call failed, skipping")
failed += 1
continue

print(" API response: " + response)
tags = parse_tags(response)
print(" Generated tags: " + ", ".join(tags))

essay["tags"] = tags
all_tags.update(tags)
existing_tags = all_tags.copy()

time.sleep(DELAY)

write_file(json_path, json.dumps(essays, ensure_ascii=False, indent=2))
print("\nUpdated: " + json_path)

save_tags(tags_file, all_tags)
print("Tags saved to: " + tags_file)

if failed:
print("Warning: {} entries failed due to API errors".format(failed))


if __name__ == "__main__":
main()

经过一番调试和漫长的等待(毕竟每条说说都要调用 API,还得加上延时避免触发限流),所有说说的标签终于重构完成。这种做法的好处显而易见:我不需要在编辑器中手动为每条说说打标签,也不需要忍受付费 AI 的长上下文计费,每次调用都是独立的、轻量的、成本可控的。搞定自动标签脚本后,我趁热打铁,开始编写 essay.py 来替代老旧的 shuoshuo.py

老脚本的功能太原始了——只能在终端里追加一条纯文本说说以及当前时间,既没有标签支持,也没有统一的数据格式。新脚本 essay.py 要做的事情很明确:接收命令行参数作为说说内容,自动获取当前时间作为发布时间,生成递增的数字 key 作为唯一标识,然后调用大语言模型 API 为这条新说说生成标签:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
import sys
import os
import json
import time
import datetime
import urllib.request
import urllib.error

API_URL = "https://api.deepseek.com/chat/completions"
API_KEY = ""
MODEL = "deepseek-v4-flash"

SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
ESSAY_JSON = os.path.join(SCRIPT_DIR, "essay", "essay.json")
TAGS_FILE = os.path.join(SCRIPT_DIR, "essay-tags.md")

PROMPT_TEMPLATE = """请阅读以下动态内容,为这条动态生成 3 个社交媒体风格的标签。
要求:
1. 标签要像社交媒体的 hashtag,简洁、有话题感,适合日常分享
2. 只输出标签,用中文逗号分隔,不要输出其他任何内容
3. 格式示例:日常,美食,生活感悟
{existing_tags_section}
动态内容:
"""


def read_file(path):
with open(path, "r", encoding="utf-8") as f:
return f.read()


def write_file(path, content):
with open(path, "w", encoding="utf-8") as f:
f.write(content)


def load_essays():
if not os.path.exists(ESSAY_JSON):
return []
return json.loads(read_file(ESSAY_JSON))


def save_essays(essays):
write_file(ESSAY_JSON, json.dumps(essays, ensure_ascii=False, indent=2))


def load_existing_tags():
if not os.path.exists(TAGS_FILE):
return set()
content = read_file(TAGS_FILE).strip()
if not content:
return set()
return set(line.strip() for line in content.split("\n") if line.strip())


def save_tags(all_tags):
write_file(TAGS_FILE, "\n".join(sorted(all_tags)) + "\n")


def build_prompt(essay_content, existing_tags):
if existing_tags:
tags_str = "、".join(sorted(existing_tags))
section = "4. 以下是本站已有的标签列表,请优先从中选择合适的标签复用,只有当已有标签无法概括时才生成新标签:\n" + tags_str
else:
section = ""
return PROMPT_TEMPLATE.format(existing_tags_section=section) + essay_content


def call_api(essay_content, existing_tags):
headers = {
"Content-Type": "application/json",
"Authorization": "Bearer " + API_KEY,
}
payload = {
"model": MODEL,
"messages": [{"role": "user", "content": build_prompt(essay_content, existing_tags)}],
"temperature": 0.3,
}
body = json.dumps(payload).encode("utf-8")
req = urllib.request.Request(API_URL, data=body, headers=headers, method="POST")
try:
with urllib.request.urlopen(req) as resp:
result = json.loads(resp.read().decode("utf-8"))
return result["choices"][0]["message"]["content"].strip()
except urllib.error.HTTPError as e:
print("API HTTP error: {} {}".format(e.code, e.reason))
sys.exit(1)
except urllib.error.URLError as e:
print("API request failed: {}".format(e.reason))
sys.exit(1)


def parse_tags(response):
if not response:
return []
for sep in [",", ","]:
if sep in response:
tags = [t.strip().strip('"').strip("'").strip("#") for t in response.split(sep)]
tags = [t for t in tags if t]
return tags[:3]
return [response.strip().strip('"').strip("'").strip("#")][:3]


def main():
if len(sys.argv) < 2:
print("Usage: python3 essay.py <说说内容>")
sys.exit(1)

content = " ".join(sys.argv[1:])
now = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")

essays = load_essays()
next_key = str(max((int(e.get("key", 0)) for e in essays), default=0) + 1)

existing_tags = load_existing_tags()

print("Content: " + content)
print("Time: " + now)
print("Key: " + next_key)
print("Existing tags count: " + str(len(existing_tags)))

print("Calling API for tags...")
response = call_api(content, existing_tags)
print("API response: " + response)

tags = parse_tags(response)
print("Generated tags: " + ", ".join(tags))

entry = {
"date": now,
"content": content,
"key": next_key,
"tags": tags,
}

essays.append(entry)
save_essays(essays)
print("Appended to: " + ESSAY_JSON)

all_tags = existing_tags | set(tags)
save_tags(all_tags)
print("Tags saved to: " + TAGS_FILE)


if __name__ == "__main__":
main()

最关键的是,它要和 auto-essay-tags.py 共用同一个标签库 essay-tags.md,这样才能保证新增说说时能充分利用已有的标签体系,避免同一个意思的标签反复出现。脚本还会将新说说追加到 essay.json 文件中,同时更新标签库。整个流程一气呵成,我只需要在终端里输入:

1
python3 essay.py 今天天气真不错!

剩下的时间戳生成、API 调用、标签解析、文件追加全部自动完成。这比我以前手动编辑 YAML 文件、手动写标签的方式高效了不知多少倍。在编写这两个脚本的过程中,我对当前大语言模型行业的商业逻辑产生了不少困惑。几乎所有模型厂商都在不遗余力地推广长上下文功能,鼓励用户把几十万甚至上百万 Token 的文档一次性丢给 AI 处理,各种支持长上下文的 Agent 工具也因此层出不穷。

表面上看这是技术进步,但细想一下就会发现其中的矛盾:长上下文真的永远是最高效的解决方案吗?以我的使用场景为例,为一条说说生成标签,只需要把这一条说说的内容传给 AI 就够了,附带一大堆无关的历史记录反而会稀释有效信息、增加 Token 消耗。行业内不是没有人意识到这个问题。

正确的做法应该是像我的脚本这样:让 AI 先生成一个调用无状态 API 的脚本框架,然后这个脚本每次只传递最小必要信息给模型。OpenClaw 之类的项目如果真往这个方向走,我怀疑它们可能根本火不起来,因为这直接动摇了厂商们靠长上下文消耗 Token 来赚钱的根基。从这个角度看,当下的 AI 生态某种程度上是被商业模式扭曲了的——技术上的最优解,未必是商业上的最优解。