很多 AI 产品最早看起来都像一个聊天框。
用户输入问题,服务端把上下文和问题发给模型,前端流式显示回答。这个体验一旦跑通,会很容易让人产生一种错觉:AI 功能已经完成了。
我在 StockTracker 里做 AI 对话时,一开始也差不多是这个思路。它是一个本地优先的个人投资记录和投研工具,用户有自己的持仓、交易记录、收益口径、行情数据和分析历史。最直接的做法,就是把这些数据整理成上下文,然后让模型回答。
但做着做着,我越来越觉得,聊天框只是入口。
真正难的是:用户的每一次提问,系统到底能不能理解成一个可执行、可追踪、可恢复的工作流。
聊天框很容易,连续性很难
接入一个模型并不难。
前端做一个输入框,服务端封装一个 OpenAI-compatible 或 Anthropic-compatible 请求,再把流式结果推回页面,一个基本的 AI 对话就能工作。
但投资分析不是普通闲聊。用户问的问题往往和一组本地状态有关:
- 当前持有哪些股票
- 每只股票的成本和仓位是多少
- 最近做过哪些买卖
- 行情是否有效
- 用户刚才问过什么
- 上一轮系统是否要求用户澄清
如果这些状态没有被系统显式管理,聊天框就会变成一个很薄的界面:看起来能对话,但每一次请求都像重新开始。
比如用户问:
帮我看看平安。
系统可能会发现本地有中国平安,也可能发现有平安银行,甚至用户并没有持仓,只是在问外部标的。
这时候最差的做法是直接猜。
稍微好一点的做法是让模型回答:“你指的是中国平安还是平安银行?”
但真正的问题在下一轮。
当用户回复“前一个”或者“中国平安”时,系统能不能接住这次澄清?能不能知道这不是一个新的闲聊问题,而是上一轮分析流程的延续?能不能跳过重复识别,直接把候选标的绑定到接下来的行情、持仓和交易复盘里?
这就是聊天框和工作流的差别。
AI 对话不是一条 prompt,而是一段状态机
我后来更愿意把 StockTracker 的 AI 对话看成一个状态机,而不是一个 prompt。
一次用户输入进来,系统要先判断它处在什么状态:
普通问题 -> 规划需要哪些数据 -> 执行 Skill -> 组装上下文 -> 生成回答需要澄清的问题 -> 保存候选项和澄清状态 -> 等待用户选择用户完成澄清 -> 消费上一轮状态 -> 继续原来的分析任务固定分析任务 -> 使用稳定的数据准备和输出结构 -> 持久化分析历史
这套东西如果不显式建模,就会散落在很多地方。
前端记一点 pending state,API route 里补一点判断,prompt 里再写几句“如果不确定就询问用户”。短期看能跑,长期看会越来越难维护。
因为你很难回答几个基础问题:
- 当前这轮回答是从哪里来的?
- 它用了哪些本地数据?
- 哪一步判断用户在问哪只股票?
- 澄清状态保存在哪里?
- 如果回答错了,是模型错了,还是数据准备错了?
当 AI 功能只是 demo 时,这些问题不明显。等它变成日常工作流的一部分,问题就会集中爆发。
固定入口和自由对话不是同一种东西
StockTracker 里有几类 AI 能力:
- 自由对话
- 组合分析
- 个股分析
- 大盘分析
- 分析历史
- Agent Trace 调试
从界面上看,它们都和 AI 有关,但内部形态其实不一样。
自由对话更像开放入口。用户可能问“成都银行走势健康吗”,也可能问“最近有哪些持仓拖累收益”,还可能问“道氏理论是什么”。它需要先理解意图,再按需调度数据。
组合分析和个股分析则更像固定任务。系统知道要读取哪些数据,也知道最后输出应该包含哪些结构,比如成本收益、仓位风险、行情位置、交易复盘和风险提示。
如果把这些入口都当成“拼一段 prompt 然后问模型”,后面会很容易混乱。
固定任务需要稳定的输出契约,自由对话需要灵活的规划能力。前者更重视可重复,后者更重视理解当前问题。它们可以复用 Agent Runtime 和 Skill,但不应该共享一坨含糊的上下文拼接逻辑。
所以我后来把很多能力往更明确的链路上收:
用户问题 -> Planner 识别意图、实体和数据需求 -> Executor 调用受控 Skill -> Context Composer 组装最小必要上下文 -> LLM 生成回答 -> Trace 记录运行过程
固定分析任务也尽量走同一套数据准备方式,只是它的 Plan 更稳定,输出格式也更稳定。
这样做的好处是,系统不会因为入口不同就产生多套事实来源。
流式回答只是体验,持久化才是工作流
AI 对话里,流式输出很容易让人觉得体验变好了。
字一个个出来,用户不用等完整回答生成,界面也显得更自然。
但对一个真正要用的工具来说,流式回答只是表层体验。更关键的是:这次回答能不能被保存、回看、调试和继续使用。
在投资工具里,分析结果不是一次性消费品。
用户可能今天让系统分析组合风险,过几天再回头看当时的判断;也可能想知道某次回答为什么提到了仓位集中;还可能想比较不同时间点的 AI 分析有没有变化。
如果系统只把最终文本存下来,就会丢掉很多重要信息:
- 当时使用的是哪些持仓数据
- 行情数据来自哪个时间点
- Planner 识别出的意图是什么
- 调用了哪些 Skill
- 上下文大概消耗了多少 token
- 哪一步失败或者 fallback 了
所以我觉得 AI 产品里的历史记录,不应该只是聊天记录。
它更应该是一次工作流运行结果的索引。
普通用户看到的是自然语言回答;开发者或者高级用户需要排查时,可以看到背后的 Trace。这样系统才有机会从“模型说了一段话”,变成“系统完成了一次可审计的分析”。
澄清不是失败,而是产品能力
很多 AI 产品会把澄清看成一种不够聪明的表现。
好像模型应该尽量猜中用户意图,少问问题,才能显得智能。
但在投资场景里,我更愿意让系统在不确定时停下来。
因为猜错标的的成本很高。
用户说“平安”,系统如果把平安银行当成中国平安,后面的持仓、行情、估值、新闻和交易复盘都会错。回答写得越流畅,误导性反而越强。
所以澄清不应该被当成异常路径。它应该是一等状态:
识别到多个候选 -> 返回候选项 -> 保存 pending clarify -> 用户选择 -> 恢复原任务继续执行
这里最关键的是“恢复原任务”。
如果用户澄清之后,系统只是重新拿这句话去跑一遍普通 Planner,那就没有真正管理状态。用户本来是在回答系统的问题,系统却把它当成一个新的问题。
这种体验会让对话变得断裂。
真正自然的 AI 对话,不是每一句都让模型自由发挥,而是系统知道哪些句子是在推进同一个任务。
Debug 页面不是开发附属品
做 StockTracker 的 Agent Trace 时,我越来越觉得调试视图不是一个可有可无的开发工具。
它会直接影响 AI 功能能不能长期迭代。
因为 AI 回答出问题时,原因通常不止一种:
- Planner 意图识别错了
- 股票名称匹配错了
- Skill 取到的数据不完整
- 行情源返回了旧数据
- Context Composer 注入了不该注入的信息
- Prompt 对边界要求不够清楚
- 模型本身生成时发挥过头
如果没有 Trace,很容易所有问题都被归结成“模型不稳定”。
然后解决方案就会变成继续堆 prompt:多写几条规则,多加几句约束,多塞一点上下文。
但很多时候,问题根本不在 prompt。
比如用户问的是单只股票,系统却把整个组合都注入进去了;或者用户问的是当前持仓,Skill 却拿到了一个外部候选标的;又或者行情已经过期,但上下文里没有标记数据时间。
这些问题只有看执行链路才能定位。
所以我现在会把 Trace 看成 Agent 产品的基础设施。它不一定要暴露给所有用户,但开发阶段必须存在。
AI 产品要从“回答”升级到“运行”
做完这些之后,我对 AI 对话产品的理解变得更保守了一点。
一个聊天框能让模型回答问题,但一个可靠的 AI 工作台,需要管理更多东西:
- 用户当前处在哪个任务里
- 系统需要哪些数据才能回答
- 哪些数据已经取到了
- 哪些地方需要澄清
- 回答用了什么上下文
- 运行过程能不能追踪
- 历史结果能不能复用
这些事情都不如模型回答本身显眼。
但它们决定了 AI 功能是一个好看的入口,还是一个能长期使用的工具。
对 StockTracker 来说,我不希望 AI 只是一个“能聊投资”的窗口。它应该能基于本地持仓、交易事实、行情数据和分析历史,完成一次次有边界、有依据、可回看、可调试的投研工作流。
所以我现在越来越觉得,做 AI 对话产品,难点真的不在聊天框。
聊天框只是人和系统之间最轻的一层界面。
真正难的是它背后那套状态:问题如何被理解,数据如何被调度,澄清如何被接住,分析如何被保存,错误如何被定位。
当这些状态能被系统认真管理时,AI 对话才不只是一次回答。
它才开始像一个产品。