我的 AI 助手说「这个做不到」,结果第二天就做到了
说实话,这事挺打脸的。
昨天我让我的 AI 助手把一篇文章发到我的 163 邮箱,它跟我说环境里没有邮件能力。我说行吧,那算了。
今天我又提了一次,它三分钟就发出来了,我邮箱里躺着一封排版整整齐齐的 HTML 邮件。
我当时就懵了。你昨天不是说做不到吗?
然后我们一起查了一下,发现工具是 7 月 31 号就写好的,凭据也早就配好了,静静躺在硬盘上等着被调用,一直到今天。
这篇就聊聊这件事,以及顺手打通的另一条链路。我觉得这里面有个坑,不只是 AI 助手会踩,做知识管理、写内部文档的同学,基本上人人都在踩。
废话不多说,我们直接开始。
一、先说为什么会「做不到」
昨天它是这么判断的:搜了一圈本地技能,搜到一个叫 lark-mail 的,打开一看是飞书邮箱,走飞书 OpenAPI,跟 163 没关系。于是得出结论——没有邮件能力。
问题就在这。
那个真正能发信的工具叫 bin/mail163.py,纯 Python 标准库写的,SMTP 发信 IMAP 收信,一个第三方依赖都没有。凭据在 .secrets/mail-163.json,权限 600,163 的客户端授权码好好地存在里面。
东西全都在。缺的是一个指向它的索引。
就像你公司 Wiki 里有一篇写得特别好的排障手册,但标题叫《关于近期若干问题的说明》,搜什么关键词都搜不到。那篇文档存在吗?存在。有用吗?没用。
我觉得这个事的本质是:能力和可发现性,是两回事。 你有能力但检索不到,跟没有能力,在结果上完全等价。
二、第一条链路:发邮件
这条其实简单到不太值得写,但有个坑我必须说。
流程就三步:
把 Markdown 剥掉 frontmatter 转成 HTML 调 mail163.py send发出去
第一遍我们发出来的邮件,长得跟记事本一样。标题不是标题,加粗不是加粗,全是一坨纯文本。
原因是这个——邮件客户端会把 <style> 标签整个剥掉。
你在网页上写 CSS 那套东西,<style> 里定义类名然后 class="xxx" 引用,在邮件里完!全!没!用! 邮件客户端为了安全,会把 head 里的样式表直接扔掉,外部 CSS 更不用想。
所以邮件 HTML 只有一条路:样式必须内联写在每个元素的 style= 属性上。
<!-- 这样没用 --><style>.title { font-size: 21px; }</style><h1class="title">标题</h1><!-- 这样才行 --><h1style="font-size:21px;line-height:1.45;color:#1a1a1a;">标题</h1>丑吗?挺丑的。但这是邮件这个二十年前的东西定下的规矩,你不服也得照做。
顺便说几个实测出来的细节,直接抄就完事了:
外层浅灰底 + 内层白卡, max-width:680px,手机上不会撑爆字体栈里一定要带 PingFang SC和Microsoft YaHei,不然 Windows 客户端字体会回退到很难看的宋体正文 15px, line-height:1.85,这个行高是我调了几次才舒服的正文长的时候用 --body-file传文件,别用-b传字符串。命令行传长文本 shell 转义会搞坏你的内容,而且还会进 shell history
还有最后一步,很多人会漏:发完必须回查。
python3 bin/mail163.py list -n 3因为 SMTP 返回成功,只代表服务器把你的邮件收下了,不代表它投递成功了。这两件事差得远。自己发给自己的话,收件箱能直接看到,一秒就验完了。
这个坑我之前踩过一个更狠的版本,写过一篇《HTTP 200 不等于数据可用》,一个道理——状态码告诉你「我收到了」,不告诉你「事办成了」。
三、第二条链路:把文件传进 AI 知识库
这条就有意思多了。
我用腾讯的 ima 做知识库,里面有个「AI 编程实战」库,攒了十几篇 CRM 项目的需求文档和技术方案。我想把之前做的那份 PPT 也传进去。
官方 API 的完整链路是五步:
① preflight 类型检查 → 拿到 media_type、file_size、content_type② check_repeated_names 重名检查③ create_media 换一份临时 COS 凭据④ 用临时凭据把文件传到 COS⑤ add_knowledge 正式入库五步里每一步都要传 JSON、解 JSON、把上一步的字段抠出来喂给下一步。第三步返回的那坨 COS 凭据有十个字段,secret_id、secret_key、token、bucket_name、region、cos_key、start_time、expired_time……
第一次跑通的时候,我是真的佛了。
坑一:版本旧了,它直接拒绝干活
第一个请求发出去,返回了个 -200:
发现新版本 skill:1.1.9(当前版本:1.1.8)注意这不是警告。这是拒绝。 你的请求根本没发出去,服务端每天检查一次版本,旧了就不给你干活。
我觉得这个设计挺硬的,但站在服务方角度也能理解——旧版本客户端的行为不可控,不如直接掐掉。
升级本身不难,下 zip 覆盖就行。但这里有个必须注意的地方:
rsync -a --delete \ --exclude='_meta.json' \ --exclude='_skillhub_meta.json' \ /tmp/ima-new/ima-skill/ ima-skills/那两个 exclude 不!能!省! 因为 --delete 会清掉目标目录里的旧文件,而这两个是技能商店的安装记录。删了之后商店就认不出这个技能了,你会莫名其妙地发现它「消失」了。
还有个小细节:官方 zip 是 macOS 打的包,里面塞满了 __MACOSX 目录和 .DS_Store 文件。解压完先清一遍再覆盖,不然你的技能目录会很脏。
坑二:返回的不是一个 JSON,是两个粘在一起的
这个坑非常非常非常隐蔽。
每天第一次调用的时候,它会把版本检查的结果直接拼在业务响应前面,长这样:
{"current_version":"1.1.9","checked_at":"..."}{"code":0,"msg":"success","data":{...}}看出问题了吗?这整体不是合法 JSON。 你 json.loads 下去,直接抛异常。
更阴的是,它只在每天首次调用时这样。你白天调试得好好的,第二天早上第一个请求就炸,你还以为是自己代码写坏了。
解法是别用 json.loads,改用 raw_decode 一个一个往下解,然后取最后一个带 code 字段的对象:
def_json_objects(text): dec = json.JSONDecoder() out, i, n = [], 0, len(text)while i < n:while i < n and text[i] notin"{[": i += 1if i >= n:breaktry: obj, end = dec.raw_decode(text, i)except ValueError: i += 1continue out.append(obj) i = endreturn out我为这几行写了个单测,专门喂它拼接的字符串。这种偶发性的坑,一定要写测试锁住,不然过两个月你自己都想不起来为什么这里不能用 json.loads。
坑三:上传失败了,千万别往下走
这条是四道安全门里最重要的一道。
第四步 COS 上传如果失败了,第五步 add_knowledge 绝对不能调。
为什么?因为这时候 COS 上可能已经有一个残缺的对象了,但知识库里没有对应条目。它变成了一个孤儿文件——占着存储,谁也看不见,谁也删不掉,你甚至不知道它存在。
所以脚本里这一步是硬停:
if r.returncode != 0: die(f"COS 上传失败,未入库(不会产生孤儿条目):{r.stderr[:300]}")顺手说另外三道门:
类型检查要先跑。视频文件、B 站/YouTube 链接、 file://路径,全都不支持。不支持就直接告诉用户,别问「那你还要不要试试」,这种问题问了纯属浪费大家时间文件名不许改。 add_knowledge的title必须严格等于file_name,包括扩展名。不许缩短、不许翻译、不许「优化」重名检查必须在换凭据之前。而且 ima 不支持替换,只能「保留两者」(文件名追加时间戳)或者取消
四、然后我们把它封装了
五步链路手工跑一次,光是往上下文里灌的 JSON 就有四五百字节。对 AI 助手来说这就是纯烧钱。
所以我让它把这套东西封成一个脚本,现在长这样:
python3 bin/ima_kb.py list-kb # 列全部知识库python3 bin/ima_kb.py ls"AI编程实战"# 看里面有什么python3 bin/ima_kb.py search "CRM" --kb "AI编程实战"# 库内搜索python3 bin/ima_kb.py upload 报告.pptx --kb "AI编程实战"# 上传python3 bin/ima_kb.py add-url <链接> --kb "AI编程实战"# 加网页/公众号文章全部用知识库名字调用,一个 ID 都不用碰。
上传成功就回一行:
已添加到知识库「AI编程实战」✓ AI驱动CRM系统实战演示-V2.pptx从四五百字节的 JSON 噪音,变成一行中文。你就说爽不爽吧。
而且四道安全门全都焊死在脚本里了,不是靠调用的人记得去检查。这点我觉得比省 token 更重要——规则写在文档里,靠人自觉执行,早晚会漏;规则写进代码,物理上就绕不过去。
顺手也把邮件那条链路封了,加上一个复盘流程的固化,今天一共落了三个技能文件。以后再有这类需求,直接一条命令,有手就行。
五、我觉得最值得说的其实是这个
回到开头那个问题:为什么昨天做不到,今天做到了?
因为昨天缺的不是能力,是索引。
我把这个想通之后,觉得它的适用范围比 AI 助手大得多:
你们公司的 Wiki 里,有多少篇写得很好但没人找得到的文档?
我见过太多这种情况了。运维同学花两天写了一份特别扎实的故障处理手册,命名叫《系统维护相关说明 V3 final》,放在一个叫「技术资料」的文件夹里,然后就再也没人打开过。三个月后同样的故障又发生,新来的同事从零开始查了一整天。
那份文档存在吗?存在。创造价值了吗?零。
所以我现在做知识沉淀,判断标准变了。以前问「这个记下来了吗」,现在问三个问题:
下次遇到这事,我会用什么词去搜? 这个词在文档标题或者描述里吗? 这份文档的入口,是在我一定会经过的路上吗? 还是需要我特意想起来去翻? 它有没有告诉我「什么时候该用它」? 光写「怎么做」不够,得写清楚「什么情况下你需要这个」
第三条最容易被忽略,但它恰恰是最关键的。 一份手册如果只写了操作步骤,没写触发条件,那它只能服务于「已经知道要用它」的人。可问题是——已经知道的人,本来也不太需要手册。
我们今天写的那三个技能文件,每一个开头都有一段「什么时候用我」,而且 mail-163 那份第一句话就是:
⚠️ 不要和
lark-mail搞混:那是飞书邮箱,和 163 完全无关。
就是专门防昨天那个误判的。踩过的坑要写在最显眼的地方,不是塞在附录里。
六、能抄走的清单
技术层面:
邮件 HTML 必须内联样式, <style>标签会被客户端剥掉发信后必须回查收件箱,SMTP 成功 ≠ 投递成功 正文长用文件传参,别塞命令行 覆盖式升级第三方组件,先想清楚哪些文件不能被 --delete干掉外部 API 可能返回多个拼接的 JSON,尤其是「每天首次」这种边界 多步上传链路,任何一步失败都要硬停,不能让残留数据留在半路 偶发性的坑,写单测锁住
方法层面:
能力 ≠ 可发现性。 找不到的能力等于没有 检索失败别急着下结论说「做不到」,先确认自己是不是搜错了词 规则写进代码,别写进「我记得」 沉淀文档时,先想清楚下次的自己会用什么词来搜它 每份文档开头写「什么时候用我」,比写「怎么用」更重要
说实话,今天最有价值的产出不是那三个技能文件,是想通了「昨天为什么做不到」。能力盘点这件事,比能力建设更容易被忽略,但往往更值钱。
你们公司的知识库,是不是也躺着一堆找不到的好东西?
夜雨聆风