从Qwen-Agent到Nanobot搭建股票ChatBI助手

如何搭建一个股票 ChatBI 助手写在前面记录AI Coding 过程及经验分享：从数据收集、数据入库、智能体原型验证，到框架迁移和 Web 界面调试。理解业务需求，先做轻量级 demo 验证，再做架构迁移与体验完善。项目目标股票分析场景下的 ChatBI （Chat Business Intelligence）助手：把数据采集、存储、分析、预测和可视化串在一起，让用户可以直接通过自然语言完成查询和分析。当前项目围绕以下几只股票做本地验证：贵州茅台五粮液广发证券中芯国际目标中的典型能力包括：历史行情查询：收盘价、成交量、成交额、涨跌幅等多股票对比：区间涨跌幅、走势对比、统计汇总自动可视化：根据查询结果自动生成折线图或柱状图智能预测：基于 ARIMA 预测未来 N 个交易日价格技术指标分析：使用布林带识别超买、超卖和异常点当前版本 主要聚焦于本地 SQLite 数据库、历史行情查询、ARIMA、布林带和 Gradio 界面。后续可开发像“实时新闻查询”等能力。本项目用到的工具与框架 1. nanobot 这个项目最终运行在 nanobot 框架上。它可以理解为一个偏工程化、偏运行时的智能体骨架，一个轻量级的 openclaw 。具备以下特点：配置驱动，模型、Provider、工具开关都可以放到 config.json 运行链路清晰，有 AgentLoop、上下文构建、工具循环这些明确的边界很适合做本地 CLI 型智能体，调试时比黑盒框架更容易定位问题 2. Tushare 股票历史数据来源使用 Tushare获取。通过 TUSHARE_TOKEN 环境变量读取密钥拉取指定股票从 2020-01-01 到当天的历史日线数据先保存为 .xlsx 再写入本地 SQLite 数据库这样做的好处是简单、可控、便于复现，也特别适合本地开发和提示词迭代。 3. SQLite 本地验证阶段使用 SQLite。零部署易于迁移和备份查询可控适合和 Pandas、SQLAlchemy、脚本工具链一起使用 4. Qwen-Agent Qwen-Agent 的优势是结构相对直接，非常适合快速做一个带工具调用的聊天型助手，且功能完善，提供工程化抽象和开箱即用高级组件。代码量少，容易看懂 Assistant + tool + WebUI 的组合很适合快速试错适合把“自然语言 -> SQL -> 查询结果 -> 可视化”这条链先跑通在本项目里，Qwen-Agent 更多承担的是 轻量原型框架 的角色。 5. Codex、Trae、Cursor 等 AI 编程工具 AI编程工具是AI开发的核心，或者说是新一代编程的核心，它让人由写代码转化为了读代码，提供思路，框架。但是AI编程的幻觉，针对复杂开发推荐使用TDD模式，本次开发较为简单，由Codex 直接完成。 6. 大模型 API 本项目使用的是阿里云百炼的 Qwen 系列模型，密钥通过环境变量 DASHSCOPE_API_KEY 注入。为什么先用 Qwen-Agent，再迁移到 Nanobot 很多时候我们一上来就会问：到底该选哪个框架？但真正更有效的问题其实是：当前阶段我最需要的是“快速验证”，还是“长期扩展”？对于这个项目：在原型阶段，我更需要快速验证在迁移阶段，我更需要更清晰的运行架构因此路线就自然变成了： Qwen-Agent 负责“跑通思路”，Nanobot 负责“承接工程化版本”。当然，不止是Nanbot框架，可以根据需要迁移到不同的框架，我们需要提供一个能跑通的demo，让AI学习新框架的用法，在把当前版本代码迁移。 Qwen-Agent 框架 Qwen-Agent 框架 在本项目中的用法在本项目原型阶段，Qwen-Agent 的角色主要是：接收用户关于股票查询的自然语言问题根据提示词理解数据库结构和查询口径生成 SQL 或调用封装好的工具自动返回表格和图表这种方式特别适合早期验证，因为它足够直接，改动反馈也快。你可以把它理解成一个很适合做“第一版能跑起来 demo”的框架。 Nanobot 架构简介 Nanobot 架构 关键模块理解 config.json 这个文件定义了模型、Provider 和工具开关，是整个项目的配置入口。例如当前项目里最关键的是这几个部分： { "agents": { "defaults": { "model": "qwen-plus", "context_window_tokens": 32768, "max_tool_iterations": 20, "timezone": "Asia/Shanghai" } }, "providers": { "dashscope": { "api_key": "" } }, "tools": { "web": { "enable": false }, "exec": { "enable": true, "timeout": 120 } } } 当前版本重点依赖本地脚本执行，因此 exec 打开，web 暂时关闭。 AgentLoop AgentLoop 是整个运行时的核心引擎。它把消息总线、模型、工作目录、工具执行限制、上下文长度和时区等能力统一串起来。本项目中的核心代码如下： loop = AgentLoop( bus=MessageBus(), provider=provider, workspace=WORKSPACE, model=defaults.model, max_iterations=defaults.max_tool_iterations, context_window_tokens=defaults.context_window_tokens, max_tool_result_chars=defaults.max_tool_result_chars, web_config=config.tools.web, exec_config=config.tools.exec, restrict_to_workspace=False, timezone=defaults.timezone, ) 从这段代码就能看出，Nanobot 比较适合做“边界清楚的智能体系统”。 ContextBuilder ContextBuilder 类把系统规则、技能说明、当前记忆和环境上下文拼接成模型真正看到的 System Prompt。本项目里这样实现类似功能，在启动时写入时间上下文： def inject_time_context() -> None: memory_file = WORKSPACE / "memory" / "MEMORY.md" today = datetime.now().strftime("%Y-%m-%d") memory_file.write_text( "# 当前时间上下文\n\n" f"当前日期：{today}\n" "股票日线数据为已收盘数据；预测与技术指标仅供学习和分析参考。\n", encoding="utf-8", ) AgentRunner / LLM + 工具循环从本质上看，Nanobot 的执行过程依然是“模型判断是否需要工具，再进入工具循环”。本项目中的 ChatBIHook 把工具调用过程打印出来，方便观察执行过程： class ChatBIHook(AgentHook): async def before_execute_tools(self, ctx: AgentHookContext) -> None: for tool_call in ctx.tool_calls: args_text = str(tool_call.arguments)[:160] print(f" [{tool_call.name}] {args_text}") 项目具体实现步骤第一步：用 Tushare 准备数据让 AI 帮我写出一个数据采集脚本，从 Tushare 拉取四只股票从 2020-01-01 到今天的历史价格，并输出到 Excel 和 SQLite。这里的思路非常直接： STOCKS = { "600519.SH": "贵州茅台", "000858.SZ": "五粮液", "000776.SZ": "广发证券", "688981.SH": "中芯国际", } 然后通过 TUSHARE_TOKEN 读取密钥： def get_tushare_token() -> str: token = os.environ.get("TUSHARE_TOKEN") if not token: raise RuntimeError("环境变量 TUSHARE_TOKEN 未设置或为空。") return token 最后统一按时间升序排序，写入 Excel 和 SQLite。这一步很重要，因为后面的所有自然语言查询、预测和可视化，都是建立在数据准备正确的前提上。第二步：生成建表 SQL，并把数据落入 SQLite 建表语句很简单，但它明确了后续整个项目的数据边界： CREATE TABLE IF NOT EXISTS stock_daily_price ( stock_name TEXT NOT NULL, ts_code TEXT NOT NULL, trade_date TEXT NOT NULL, open REAL, high REAL, low REAL, close REAL, pre_close REAL, change REAL, pct_chg REAL, vol REAL, amount REAL, PRIMARY KEY (ts_code, trade_date) ); 对应文件是： create_stock_daily_price.sql 这张表的设计虽然不复杂，但已经足够支撑当前版本的大部分能力：历史价格查询涨跌幅分析成交量和成交额分析时间序列建模布林带计算第三步：先用 Qwen-Agent 做出“能聊、能查、能画图”的原型在这个阶段，我并没有急着做复杂架构，而是先做一个轻量级原型，把“自然语言 -> 数据库 -> 结果可视化”跑通。这一阶段的经验可以概括成一句话：不要一开始就追求完美架构，先验证交互链路和分析口径。原型里最重要的并不是代码写得多复杂，而是把下面几件事说清楚：数据库在哪表名是什么用户问“价格”时默认查什么字段用户问“区间涨跌幅”时如何计算哪些 SQL 允许执行，哪些绝对不允许这也是为什么在这类项目中，Prompt 设计的价值非常高。第四步：不断打磨 SQL 工具的返回结果早期版本中，我对 SQL 工具做了几轮非常实用的增强：查询结果只有一行时，不做统计，也不画图 DataFrame 预览不只看前 10 行，而是更综合地展示头尾信息结果返回中加入描述统计，帮助模型更准确地总结自动根据数据规模选择折线图或柱状图横坐标做抽样显示，避免图表标签挤成一团这些小改动看起来不起眼，但对用户体验和模型最终回答质量影响非常大。真正让一个查询助手“好用”的，往往不是会不会写 SQL，而是它是否知道：什么情况该画图什么情况不该画图什么情况需要统计摘要什么情况应该把上下文压缩得更适合模型理解第五步：补上 ARIMA 预测和布林带分析在基础查询跑通之后，我继续加入了两个比较典型的分析能力： ARIMA 当前项目的 ARIMA 技能说明中，约定使用最近一年收盘价，基于 ARIMA(5,1,5) 预测未来 N 个交易日价格。技能入口命令是： python skills/arima-predict/scripts/run_arima.py --ts-code 600519.SH --n 7 它适合做趋势性预测验证，但必须明确写清楚：预测结果仅供学习和分析参考，不构成投资建议。 Bollinger 布林带工具使用的是 20 日周期 + 2σ，用于检测超买和超卖点。技能入口命令是： python skills/bollinger/scripts/run_bollinger.py --ts-code 600519.SH 布林带分析很适合做异常点可视化，也能让这个项目从“纯查询助手”进一步扩展为“带技术指标分析能力的助手”。第六步：从 Qwen-Agent 迁移到 Nanobot 当整个思路在 Qwen-Agent 中被验证得比较清楚之后，我开始把项目迁移到 Nanobot 版本。迁移的重点不是“把代码原样搬过去”，而是重新梳理运行结构：配置归拢到 config.json 模型 Provider 通过 DashScope 注入通过技能文件约束不同分析能力的调用方式利用 AgentLoop 承接整个运行时保留 CLI 作为最稳定的调试入口当前项目主入口是： agent.py 其中最核心的构造函数是： def build_bot() -> Nanobot: dashscope_key = os.environ.get("DASHSCOPE_API_KEY", "") config = load_config(WORKSPACE / "config.json") config.providers.dashscope.api_key = dashscope_key config.agents.defaults.workspace = str(WORKSPACE) provider = _make_provider(config) defaults = config.agents.defaults loop = AgentLoop( bus=MessageBus(), provider=provider, workspace=WORKSPACE, model=defaults.model, max_iterations=defaults.max_tool_iterations, context_window_tokens=defaults.context_window_tokens, max_tool_result_chars=defaults.max_tool_result_chars, web_config=config.tools.web, exec_config=config.tools.exec, restrict_to_workspace=False, timezone=defaults.timezone, ) inject_time_context() return Nanobot(loop) 这一步让我对整个项目的“运行时结构”有了更强的掌控感。第七步：补上 Gradio WebUI 最后一步，是把 CLI 助手接到一个更适合演示的 Web 界面上。当前界面入口在： webui.py 这个阶段最有意思的，其实不是写 UI 本身，而是调试： Gradio 5 和 Gradio 6 的参数不兼容图片本地路径在浏览器端 404 不同 Conda 环境里的依赖版本不一致 Chatbot 的消息格式和复制按钮参数在不同版本下不同最后的解决方案包括：根据 Gradio 版本动态选择 Chatbot 参数将本地生成的图表转为 Base64 内嵌图片，避免 /file=... 路由兼容问题保留独立 webui.py，不污染 CLI 入口这一步其实很能说明一个真实项目的特点：真正耗时间的，往往不是“写页面”，而是处理不同框架和依赖之间那些看似很小、但很影响体验的兼容细节。本项目中我比较认可的几个实践 1. 先做本地验证 SQLite、本地图片、本地脚本、本地 CLI，这些选择看起来“朴素”，但非常适合项目初期。因为在这个阶段，最重要的是减少外部变量，把问题压缩到真正需要思考的地方。 2. 先原型，后迁移 Qwen-Agent 让我快速验证思路，Nanobot 让我把结构理清。这种“先轻后稳”的路线，对个人项目尤其有效。 3. 用技能文件固化分析规则当前项目里把历史 SQL、ARIMA、布林带分析分别写成技能说明，是一个很实用的做法。这样做的价值在于：分析口径更清晰调用命令更明确更容易约束模型行为 4. 把调试过程当成项目资产这次 Gradio 的图片问题、版本兼容问题，其实都很典型。如果只是临时修掉，它们很快会再次出现；但如果把这些问题写进复盘，后面就会少踩很多坑。一些调试心得这次项目里，我最大的感受不是“写功能有多难”，而是：真正困难的是在多框架、多依赖、多版本之间，持续把系统保持在一个可验证、可运行、可解释的状态。有几条经验我觉得特别值得记下来。 1. 先看根因，不要急着补丁式修复比如图片不显示的问题，最后根因并不是“图没生成”，而是 Gradio 对本地文件路由的处理在当前版本下返回了 404。如果一开始只盯着前端页面，很容易误判方向。 2. 版本兼容问题永远值得优先确认 Gradio 5 和 Gradio 6 的 Chatbot 参数不同，这类问题如果不先确认版本，后面会浪费很多时间。 3. 调试日志非常重要像 ChatBIHook 这种打印工具调用参数的设计，在智能体项目里非常有用。因为很多时候，模型“为什么答成这样”，必须结合它实际调了什么工具才能判断。 4. 让项目始终保留一个最稳定的入口这也是为什么我保留了 CLI。WebUI 可以坏，前端可以兼容出问题，但 CLI 往往是最稳定的诊断入口。这次项目的局限与后续方向当前版本已经可以完成：本地股票历史数据查询自动图表展示 ARIMA 预测布林带分析 CLI 交互 Gradio WebUI 交互但它仍然有很明确的边界：数据仍然是本地验证数据，不是生产级实时数据流当前没有真正接入实时新闻查询 Prophet 周期性分析尚未在这版落地 WebUI 更偏演示版，还不是成熟产品化前端如果后续继续扩展，我最想补的方向会是：接入实时行情或新闻检索能力补充 Prophet 或更多时序分析模型引入更稳定的前端状态管理和历史会话管理让技能体系进一步模块化，便于横向扩展到更多金融分析场景结语这次项目对我来说，最重要的收获不是“又做了一个股票助手”，而是把一条很完整的 AI Coding 路线真正走了一遍：从业务需求出发用 Tushare 准备数据用 SQLite 固定本地验证场景用 Qwen-Agent 快速做出可交互 demo 用 ARIMA 和布林带补足分析能力再迁移到 Nanobot，理顺运行架构最后补上 Gradio，并处理真实调试问题回头看，这个项目最值得总结的并不是某个单点技术，而是这条方法论：理解业务需求 -> 打造 skill 或工具能力 -> 用 Qwen-Agent 做轻量级 demo -> 明确边界后做架构迁移。这条路线很适合个人项目，也很适合 AI 时代的软件原型开发。如果后面我继续补实时新闻、Prophet、更多股票池、前端产品化体验，这篇文章也会成为一个很好的阶段性起点。