AI从不写数字:双通道渲染如何杜绝错误财报数据

Stock Expert AI 用双通道渲染、数字闸门和哈希校验,确保模型永不生成页面数字。
Stock Expert AI 面对大语言模型在财报总结中可能编造数字的风险,提出“双通道渲染”机制:数据通道从 FMP 原样渲染所有数字,评论通道由模型只写文字。为堵住文字中仍可能出现的数字,他们设计了数字闸门,提取草稿中每个数字并与事实包按书写精度容差比对,无依据即拒绝。第二审查员只检查窄清单(错误数字、时期、投资建议、扭曲引语),并通过 SHA-256 哈希确保已批准文本和证据不被篡改。长转录稿先摘要但闸门始终针对原始来源运行。这套组合以可机械检查的承诺替代了对模型准确性的统计信任,代价是文字更平实、依赖事实包完整性、正则提取脆弱且哈希对空白敏感。
大语言模型在总结财报时,迟早会生成一个“几乎正确”的营收数字。数字看起来合理,句子读起来通顺,但输出中没有任何标记告诉你哪些是复制来的、哪些是模型自己编的。对于一个给股票评分的网站来说,这是最致命的失败模式:一个自信的错误数字,看起来和所有正确数字一模一样。
Stock Expert AI 对这个问题给出了一个硬性规则:模型永远不允许生成任何数字。他们称之为“双通道渲染”。本文基于该团队在开发者社区的分享,拆解这套机制的原理、实现与代价。
双通道:数据与评论彻底分离
这套规则说起来很简单:AI 只写评论,页面上每一个数字都从市场数据提供商(Financial Modeling Prep,FMP)原样渲染。两个通道分工明确:
- 数据通道:从 FMP 获取的数值,由普通代码渲染。提供商响应和最终像素之间没有任何模型参与。
- 评论通道:由模型撰写的文字,负责解释、提供背景和串联信息。

把两者拆开的核心价值在于:你不再需要问“模型准确吗?”——这个问题只能靠统计回答;你开始问“有没有数字从评论通道混进了页面?”——这个问题可以机械地检查。模型可能在强调重点或语气上出错,但它不可能在页面展示的数字上出错,因为页面根本不从它那里取数字。
这覆盖了网站的结构化部分,但没有覆盖文字本身。如果你放任模型自由写作,它仍然会写出“营收增长至 460.6 亿美元”这样的句子。这就是设计的第二半要解决的问题。
FMP(Financial Modeling Prep) 是一个面向开发者的金融数据 API 服务商,提供财务报表、分析师预期、股票评级、内部交易、财报电话会议记录等结构化数据。它的核心价值在于数据经过标准化清洗,以 JSON 等机器可读格式返回,而非网页抓取得到的非结构化文本。对于 Stock Expert AI 这类应用,FMP 扮演的是“唯一可信数据源”角色:页面上的营收、利润率、EPS 等数字全部直接取自 FMP 响应,中间不经过任何模型处理。这意味着数据的正确性责任被完全转移给了数据提供商,团队只需保证渲染代码没有篡改数值,而不必评估模型输出的统计准确性。
日志管道:先有证据,再写文章
新闻板块是模型写作量最大的地方,因此围绕它的机制也最重。
选题不由模型决定。故事从 FMP 数据中挑选:财报日历、两个交易日内涨幅靠前的股票(市值下限 10 亿美元)、以及行业主题。同一只股票 7 天内不重复报道。
每个故事都会从 FMP 端点构建一个“事实包”:财务报表、分析师预期、评级、业务分部、内部交易和财报电话会议记录。然后一个中等规模的模型以这个事实包为唯一来源撰写草稿。
草稿在任何人看到之前,必须先通过一道闸门。
数字闸门:每个数字都必须有出处
闸门会提取草稿中的每一个数字,并与事实包比对,允许四舍五入容差。草稿写“460.6 亿美元”,应该匹配事实包中的 46,061,000,000,因为作者四舍五入到了两位小数。草稿写“470 亿美元”,则不应通过。
一个巧妙的技巧是:容差由数字的书写方式决定。如果草稿以十亿美元为单位显示两位小数,允许的误差就是最后一位单位的半个,再按比例放大。简化后的 TypeScript 示意代码如下:
interface Parsed {
value: number;
tolerance: number;
}
const SCALE: Record<string, number> = { k: 1e3, m: 1e6, b: 1e9, t: 1e12 };
function extractNumbers(text: string): Parsed[] {
const out: Parsed[] = [];
const re = /\$?(\d[\d,]*)(?:\.(\d+))?\s?([kmbt])?\b/gi;
for (const m of text.matchAll(re)) {
const whole = m[1].replace(/,/g, "");
const frac = m[2] ?? "";
const scale = SCALE[(m[3] ?? "").toLowerCase()] ?? 1;
const value = Number(`${whole}.${frac || "0"}`) * scale;
// half a unit in the last digit the writer chose to show
const tolerance = 0.5 * 10 ** -frac.length * scale;
out.push({ value, tolerance });
}
return out;
}
function findUnsupported(draft: string, facts: number[]): Parsed[] {
return extractNumbers(draft).filter(
(n) => !facts.some((f) => Math.abs(f - n.value) <= n.tolerance),
);
}
// "$46.06B" -> 46.06e9 with tolerance 5e6, so 46_061_000_000 passes
这段代码有意写得朴素。生产环境的闸门还要处理百分比、年份、季度标签、范围和“翻倍”这类词语。关键在于整体形状:提取所有看起来像数字的东西,要求每一个都能被证据解释,并且失败时直接拒绝。任何无依据的数字都会退回草稿,而不是被修补。
失败即拒绝还有一个有用的副作用:闸门不需要理解句子含义,它只需要知道出现了一个数字,而证据中没有任何东西能解释它。
容差(tolerance) 在此处的含义与一般数值比较不同:它不是固定的绝对误差或相对误差,而是由写作者选择的显示精度反向推导出的允许范围。当草稿写“460.6 亿美元”时,最后一位小数是 0.1 亿(即 1000 万),允许误差为半个单位即 500 万美元,因此事实包中的 46,061,000,000 可以通过。这种设计的直觉是:作者既然选择保留两位小数,就隐含承诺了该精度下的四舍五入是正确的。容差随单位缩放(k/m/b/t)同比放大,使得“4.6B”比“4.61B”拥有更宽的容忍区间。它把“数字是否可信”转化为“写作者声称的精度是否与证据一致”,从而避免了为每个数字手动设定阈值的负担。
第二审查员:正则看不到的盲区
数字闸门能抓住不存在的数字,但它无法判断一个真实数字是否挂在了错误的时期,也无法判断一句话是否滑向了投资建议,或者引语是否被扭曲。因此,一个更大的模型会审查草稿,但只针对一份很窄的清单:错误数字、错误时期、投资建议和扭曲的引语。他们不问这个模型“文章好不好”。窄清单更容易评估,也更难让审查员跑偏。
文章通过后,网站会存储已批准文本的 SHA-256 哈希和证据的哈希。只有当两个哈希仍然匹配时,文章才可被索引。如果有人编辑了文本,或者底层证据发生变化,文章就会从可索引集合中消失,直到重新获批。这把“某个时间点验证过”变成了系统在每次请求时都会检查的属性。
SHA-256 哈希 是一种密码学哈希函数,对任意输入生成固定 256 位(32 字节)的输出,具有雪崩效应:输入哪怕只改变一个空格,输出也会完全不同。在这里,团队对已批准文本和其证据分别计算 SHA-256,并存储两个哈希值。每次请求索引时,系统重新计算当前文本和证据的哈希,只有两者都与存储值匹配,文章才可被索引。这实现了“内容完整性”的自动化校验:任何文本编辑或证据更新都会使哈希失配,文章自动退出可索引集合,直到重新走完审批流程。哈希计算成本极低,适合高频检查,但它对空白字符敏感,因此需要提前确定归一化规则(如统一换行符、去除首尾空格)以避免误判。
长转录稿与原始来源规则
财报电话会议记录很长,所以一个低成本模型会在写作者看到之前先做摘要。这引入了一个明显的风险:摘要器可能编造或扭曲一个数字,而写作者会心安理得地重复它。
解决办法是一条顺序规则:数字闸门始终针对原始转录稿运行,而不是摘要。如果摘要引入了一个源文件中不存在的数字,闸门没有东西可以匹配它,草稿就会失败。摘要错误无法把数字引入已发布的文本,因为摘要从来不是被信任的东西。
同样的思路适用于其他有损步骤:无论什么压缩或重写证据的环节,验证都必须指向未压缩的原始文件。
有损压缩(lossy compression) 在本文语境下指任何会丢失或扭曲原始信息的中间步骤,例如用低成本模型对长转录稿做摘要、对事实包做精简、或对证据做格式化重写。团队的原则是:验证必须始终指向未压缩的原始文件(如完整转录稿),而不是压缩后的产物。原因是压缩步骤本身可能引入错误(如摘要器编造数字),如果闸门只对照摘要验证,就会把摘要的错误当作事实基准,从而放行虚假数字。这条规则可以推广到任何“先压缩再使用”的管道:只要最终输出依赖某个中间表示,校验就必须回溯到该表示的源头,否则压缩环节就成了信任链的薄弱点。
服务层:给爬虫 HTML,给 AI 爬虫 Markdown
爬虫拿到的是服务端渲染的 HTML。每篇文章在同一 URL 加 .md 还有一个纯 Markdown 副本,面向 AI 爬虫。技术栈是前端 React + Vite + TypeScript,后端 Express + TypeScript,Drizzle ORM 配 Neon PostgreSQL,前面一个 Cloudflare Worker,托管在 Replit 虚拟机上。这些都不稀奇。关键点在于 Markdown 副本来自与 HTML 相同的已批准文本,所以两者都在哈希检查范围内。
模型调用全部以扁平订阅方式无头运行,他们按任务选择最便宜的够用模型:摘要用便宜的,写作用中等的,审查用更大的。
权衡与反思
文字变得更平实。 一个不能引用事实包之外数字的模型,会写出更少的生动比较。有些草稿被拒绝,是因为数字技术上真实但属于推导值,比如模型自己计算的增长率。他们把推导数字视为模型在做算术,而这恰恰是规则禁止的,所以解决办法是在代码中计算好并放进事实包。
闸门的好坏取决于事实包。 如果 FMP 缺少某个字段,一个真实的陈述也会被闸门拒绝。他们接受错误拒绝而非错误批准,但这确实影响吞吐量。
提取是最脆弱的部分。 基于正则的数字提取需要围绕格式持续维护。如果重新开始,他们会更早定义数字的结构化表示,让写作者输出带标签的占位符,由代码填入数值,而不是从自由文本中反向解析数字。那会让两个通道比现在更严格。
哈希便宜但不宽容。 已批准文本的任何空白级别改动都会使其失效。这正是他们想要的行为,但意味着归一化规则需要提前决定。
核心结论
核心想法很小:不要把你无法承受错误的东西托付给模型,并且要针对原始来源验证,而不是针对另一个模型的摘要。一个数字闸门、一个窄范围审查员和一对哈希并不复杂,但它们共同做出一个可检查的承诺:页面上的数字来自数据提供商,而非模型。
相关推荐

uGAP:统一100+非洲地理数据集的AI平台
uGAP是开普敦大学团队开发的非洲地理空间AI平台,统一120余个非洲数据集并提供Python SDK,支持数据发现、处理与模型训练一站式工作流,已用于气旋洪水制图等实战场景。

Cursor能控制你的电脑了:AI代理可驱动桌面应用测试
Cursor 新增桌面控制能力,AI 代理可驱动 iOS 模拟器等桌面应用测试自己构建的功能。本文分析这一更新对开发工作流的影响、技术实现可能路径及可靠性考量。

Vibecoding自研推理引擎:理想丰满,现实骨感
Reddit开发者分享Vibecoding自研推理引擎的真实经历:用WebGL2和GLSL在浏览器跑GGUF Q4模型,却陷入tokenizer调试地狱。本文分析浏览器端LLM推理的工程难点,以及为何直接clone llama.cpp是更理性的选择。