
让 AI Agent 的回复在微信里排版好看:一次踩坑与网关层自动适配实战
给微信 Bot 做排版适配,我被「单个换行」坑了一下午
最近在折腾一套东西:在我的安卓手机上跑一个 AI 助手,接进微信个人号。别人在微信里发消息给我,消息转到手机上的 Agent 处理,再把回复发回微信。整条链路跑通那天还挺得意的,结果第二天就被打脸了——回复能收到,但排版惨不忍睹。
满屏的 ##、**、还有 | a | b | 这种东西。我精心组织的一大段技术分析,到微信里成了一堆井号和竖线。
我还以为是我哪里写错了,对着消息看了半天才发现:微信根本不认 Markdown。
微信是个纯文本框,仅此而已
我之前潜意识里总觉得,机器人嘛,输出 Markdown 天经地义,对面总能渲染。但这次的目标端是微信个人聊天窗口,不是网页也不是 Telegram。它就是个纯文本框。
## 标题 就显示成「井号空格标题」。**加粗** 就显示成带星号的字。表格更别提,| a | b | 直接是一排竖线。所有我以为的「格式」,在微信眼里都是普通字符。
行,那我不写 Markdown 不就行了。
结果栽在换行上
这才是真正坑我的地方。
我第一版改法很朴素:不用任何 Markdown 符号,列表项一行一个,纯文字。心想这下总干净了吧。发出去一看——所有列表项还是挤成一坨。
我对着屏幕愣了一会儿,开始怀疑人生。试了几次才反应过来:微信把单个回车当成了空格。我辛辛苦苦敲的换行,到微信那儿直接被吃掉了,前后两行黏成一行。
真正能分段的,是空行,也就是两个连续的换行 \n\n。单个 \n 等于没换。
知道这个之后我有点哭笑不得。这意味着哪怕我一个 Markdown 符号都不用,只要列表项之间只敲一个回车,照样废。
手动改了几版,全崩
搞明白原理,我开始手动适配:标题改成 【标题】,加粗改成方括号,列表项之间老老实实敲空行。
第一版发出去,看着还行。第二版有个地方忘了敲空行,塌了。改回来。第三版又有个地方漏了。又塌。
来来回回改了四五遍,每次都能找出一个漏敲空行的地方。我当时就烦了——这玩意儿靠人记是记不住的,只要发一百条消息,总有那么几条会漏。
而且这事儿本来就不该我来操心。我想要的是「随便写点 Markdown,对面自动变好看」,而不是每次发消息都像走钢丝。
让代码来干这事
烦到一定程度就想到,这种「规则固定、每次都要执行」的活儿,本来就该写代码,不该靠人。
我去翻了翻 html2text 和 markdown2 这两个老牌库的源码,想看看别人怎么处理 markdown 转纯文本。翻到就一个核心结论:它们在每个块之间统一插 \n\n。段落也好、列表项也好,块和块之间就是一个空行。跟我手动敲空行是同一个道理,只不过人家是代码干的,不会漏。
那我也照这个来。在网关发送消息之前,加一步清洗,把 Markdown 自动转成微信能正常显示的纯文本。
具体就干这么几件事:井号标题换成方括号,**加粗** 换成方括号,链接转成纯文本的「文字: 网址」(微信会自动把网址变成可点的蓝字),表格直接拆成竖排(手机上看表格必然乱),代码块的围栏去掉只留内容。最后再来一步:把所有连续换行统一成一个空行,单个孤立的换行也补成空行。
核心函数不长,我直接贴出来,标准库 re 就够,不用装任何东西。
def _wechat_text(text):
if not text:
return text
t = text
# 1) 代码栅栏 ```lang ... ``` -> 去栅栏行,保留内容
t = re.sub(r'(?ms)^```[^\n]*\n?', '', t)
t = re.sub(r'(?m)^```\s*$', '', t)
# 2) 标题 # ## ### -> 【内容】
t = re.sub(r'(?m)^\s{0,3}#{1,6}\s+(.+?)\s*#*\s*$', r'【\1】', t)
# 3) 链接 [text](url) -> text: url
t = re.sub(r'\[([^\]]+)\]\(([^)]+)\)', r'\1: \2', t)
# 4) 加粗 **x** / __x__ -> 【x】
t = re.sub(r'\*\*(.+?)\*\*', r'【\1】', t)
t = re.sub(r'__(.+?)__', r'【\1】', t)
# 5) 行内码 `x` -> x
t = re.sub(r'`([^`]+)`', r'\1', t)
# 6) 斜体 *x* -> 「x」(放在加粗之后,避免误伤)
t = re.sub(r'(?<!\*)\*([^*\n]+)\*(?!\*)', r'「\1」', t)
# 7) 表格:丢掉 |---| 分隔行,| a | b | 拆成 · a b
lines = t.split('\n')
out = []
for ln in lines:
s = ln.strip()
if re.match(r'^\|?[\s:\-\|]+\|?$', s) and '-' in s:
continue
if s.startswith('|') and s.endswith('|'):
cells = [c.strip() for c in s.strip('|').split('|')]
cells = [c for c in cells if c]
out.append('· ' + ' '.join(cells))
else:
out.append(ln)
t = '\n'.join(out)
# 8) 换行归一化:连续换行压成一个空行;孤立单换行也补成空行
t = re.sub(r'\n{2,}', '\n\n', t)
t = re.sub(r'(?<!\n)\n(?!\n)', '\n\n', t)
return t.strip()接入也很简单,就在发消息前过一遍这个函数。但我特意加了两个保险:一是清洗过程万一出异常,就直接用原文发,宁可不好看也不能把消息搞丢;二是留了个环境变量开关,哪天想看原始输出能一键关掉。
现在的样子
弄完之后我随手发了条塞满 Markdown 的消息测试。标题、加粗、表格、链接、代码全有,发到微信一看——干干净净。标题带了方括号,表格变成一行行的竖排,每段之间该空的行都在,一个多余符号都没有。
最舒服的一点是,这下底层接什么 Agent 都无所谓了。管你是 Claude 还是 Qwen,随便输出 Markdown,管道会自动收拾干净。我不用再去每个 Agent 的提示词里写一堆「请不要用 Markdown」这种话——那种约束写多了我自己都记不住, agent 也经常不当回事。
写代码强制,比写提示词求它,靠谱多了。
一句话总结
微信个人号做 Bot,就记两条:它不认 Markdown;单个换行等于没换。这两件事别指望 Agent 自己注意,在发送管道里加一步转换,用代码焊死,一劳永逸。
读者评论
评论会同步写入该文在 Notion 中的页面底部(与正文同页,便于管理)。
暂无评论,欢迎抢沙发。