Notion Blog
让 AI Agent 的回复在微信里排版好看:一次踩坑与网关层自动适配实战
技术分享1 分钟阅读

让 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 中的页面底部(与正文同页,便于管理)。

0/1500

暂无评论,欢迎抢沙发。