用Streamlit快速构建市场简报MVP:从LangChain引擎到可演示产品 1. 项目概述从后台引擎到可演示产品的关键一跃如果你已经跟着上一篇文章用LangChain和EODHD的API搭建了一个能跑通的市场简报生成引擎那么恭喜你最难的部分已经完成了。但说实话一个只能在Jupyter Notebook里运行的run_brief()函数离一个能拿给产品经理、创始人或者非技术同事看的“产品”还差得远。他们需要的是一个能点、能看、能直观感受价值的界面而不是一堆需要你解释的命令行输出。这就是我们这篇文章要解决的核心问题如何用最快的方式把那个强大的后台引擎包装成一个看起来、用起来都像那么回事的最小可行产品。为什么选择Streamlit答案就两个字速度。在这个阶段我们的目标不是打造一个像素级完美、前端架构复杂的应用。我们的目标是快速验证。Streamlit让你能用纯Python脚本在几分钟内构建出交互式Web应用无需操心HTML、CSS、JavaScript或者任何前后端分离的架构。它就像一个魔法胶水能把你的数据处理逻辑和可视化组件瞬间粘合起来变成一个可分享的链接。这对于内部演示、收集早期反馈、甚至是作为产品原型的起点是无可替代的效率工具。本教程将带你一步步构建一个双面板市场简报MVP。左边是AI生成的、易于阅读和分享的Markdown简报右边则是所有支撑这份简报的“硬数据”——价格回报、估值指标、风险数据和新闻头条。这种设计不仅让输出结果一目了然更重要的是建立了信任用户能立刻看到哪些是AI基于真实数据工具调用结果的解读哪些是纯粹的叙述。整个项目将围绕“查询优先”的理念展开因为这才是人类真实的思考方式——人们先有问题然后才考虑用哪些参数去框定它。2. 核心设计思路为何是“查询优先”与“双面板”在构建任何工具的UI时一个常见的陷阱是过早地将工程师的思维——参数化、配置化——强加给最终用户。我们之前构建的引擎函数run_brief(ticker, n_days, ...)就是一个典型的例子。它要求用户先想好股票代码、时间窗口、要不要基本面、要不要新闻。但实际工作中一个分析师或产品经理的思考起点往往是“苹果过去两个月表现怎么样最近有什么大新闻吗”2.1 从“参数配置”到“自然语言查询”“查询优先”的UI设计正是为了匹配这种自然的思维流程。我们将最主要的输入控件设计为一个大的文本区域st.text_area让用户可以直接输入类似自然语言的指令。例如“给我一份苹果过去60天的简报包含基本面和5条头条新闻。”这个查询本身已经隐含了ticker“AAPL.US”n_days60include_fundamentalsTrueinclude_newsTruenews_limit5。我们的后台引擎稍后会改造需要有能力解析这样的自然语言指令并提取或推断出必要的参数。那么侧边栏里的“默认股票代码”和“默认交易天数”滑块是干什么用的它们是安全网和兜底策略。当用户的查询过于模糊比如只写了“过去60天简报”引擎无法从中提取股票代码时就会使用这里设置的默认值。这确保了应用不会因为用户的随意输入而崩溃提升了产品的鲁棒性。2.2 “强制参数”为团队协作而生的控制层“可选参数强制包含”这个折叠区域是另一个关键设计。它默认是收起的不会干扰普通用户的简单查询。但它服务于一个重要的场景团队标准化。想象一下你的投资团队决定所有内部简报都必须包含市盈率PE和市净率PB以供快速参考。或者在风险审查工作流中必须每次都计算波动率和最大回撤。通过勾选这些复选框你可以“强制”引擎在每次查询中都包含这些模块即使用户的查询里没有明确提及。这相当于为团队定制了一套简报模板确保了输出内容的一致性而无需每个用户都记住并输入一堆复杂的指令。2.3 双面板布局叙事与数据的清晰分离输出采用左右分栏布局是本次UI升级的灵魂。左面板叙事层这里呈现AI生成的Markdown简报。它的目标是可读性和可传播性。这份简报的格式应该是干净的包含诸如“概览”、“关键指标”、“可能意味着什么”、“注意事项”等小节。用户可以直接将这部分内容复制到Slack、电子邮件或周报中作为一段现成的市场评论。右面板数据层这里以结构化的方式展示所有工具调用的原始结果。包括价格窗口的起止日期、总回报率基本面数据中的股票名称、行业、市值、PE、PB等风险指标中的年化波动率和最大回撤以及新闻头条的列表。这种分离带来了几个巨大优势建立信任数据一目了然用户无需猜测AI的结论从何而来。他们可以快速扫描右侧的数字与左侧的叙述进行交叉验证。提升扫描效率对于熟悉数据的用户他们可能只想快速看一眼关键指标比如PE是否过高波动率是否激增右侧面板提供了这种“一眼获取”的能力。便于调试作为开发者当AI生成的简报看起来不对劲时你可以立刻检查右侧的工具输出看是数据源出了问题还是AI的解读有误。最重要的是右侧面板的数据渲染不需要重新调用API。这些数据是后台智能体Agent在生成简报的过程中已经获取并使用的。我们只是将其从智能体的消息历史中提取出来直接呈现给前端。这保证了UI的响应速度并避免了不必要的API调用和费用。3. 构建Streamlit应用骨架与输入面板让我们开始动手编码。首先确保你已经安装了Streamlitpip install streamlit pandas。我们的应用结构会非常清晰遵循“前端归前端后端归后端”的原则。3.1 应用骨架设定舞台我们创建一个名为app.py的文件。开头的几行代码设定了整个应用的基调和布局。import streamlit as st import pandas as pd from copilot import run_query # 这是我们的核心后端函数 # 页面配置设置标题和采用宽布局为双面板预留空间 st.set_page_config(page_title市场简报副驾驶, layoutwide) # 应用标题和简短描述 st.title(市场简报副驾驶) st.caption(LangChain EODHD。生成简洁的内部风格简报并附带工具验证的指标。)关键点解析from copilot import run_query这是前后端分离的关键。所有复杂的逻辑——解析查询、调用智能体、使用工具——都封装在copilot.py的run_query函数里。app.py只负责展示和交互。这种设计让后端逻辑可以轻松复用于其他场景比如未来封装成FastAPI接口。layout“wide”这是一个重要的用户体验决策。标准的Streamlit布局比较窄适合单列内容。但我们计划左右分栏显示简报和指标宽布局能提供更舒适的视觉空间避免内容显得拥挤。3.2 输入面板设计用户交互的核心输入面板将放置在侧边栏st.sidebar中这是Streamlit应用的常见模式能让主内容区域更专注于输出。with st.sidebar: st.header(输入参数) # 1. 核心自然语言查询框 query st.text_area( 查询指令, value为 AAPL.US 生成过去60个交易日的简报。包含PE和PB并获取5条最新头条新闻。, height100, help请输入您的自然语言指令例如特斯拉过去一个月表现如何有什么风险 ) # 2. 兜底参数当查询指令不明确时使用 default_ticker st.text_input( 默认股票代码仅在查询未指定时使用, valueAAPL.US, help例如AAPL.US, MSFT.US, 或 TSLA.US ) default_n_days st.slider( 默认交易天数窗口仅在查询未指定时使用, min_value20, max_value180, value60, step5, help设置默认的分析时间范围 ) st.divider() # 添加一个视觉分隔线 # 3. 强制参数区团队控制层 with st.expander(可选参数强制包含, expandedFalse): # 默认收起 include_fund st.checkbox(基本面指标PE, PB, 市值等, valueFalse) include_risk st.checkbox(风险指标波动率、最大回撤, valueFalse) include_news st.checkbox(新闻头条, valueFalse) # 新闻数量滑块仅当“新闻头条”被勾选时才启用 news_limit st.slider( 头条数量, min_value3, max_value10, value5, step1, disablednot include_news ) # 4. 执行按钮 run_btn st.button(生成简报, typeprimary, use_container_widthTrue)设计逻辑与避坑指南st.text_area的高度设置一个合适的高度如100像素让用户有足够的空间输入多行查询同时又不会占据过多版面。help参数为每个输入组件添加帮助文本能极大提升新用户的使用体验减少困惑。条件禁用news_limit滑块的disablednot include_news实现了优雅的联动。只有当用户决定包含新闻时才能设置新闻数量避免了无效参数的困惑。st.expander将“强制参数”放在一个可折叠的容器中保持了UI的简洁。普通用户看到的是一个干净的查询界面而需要定制化模板的团队则可以展开进行设置。注意在真实的、面向多用户的产品中你可能会将这些“强制参数”的默认值保存在数据库或配置文件中并根据用户或团队的权限进行加载而不是每次让用户手动勾选。这里的UI设计提供了最灵活的调试和演示接口。4. 后端引擎改造从run_brief到run_query我们的前端变成了“查询优先”那么后端的接口也必须相应改变。原来的run_brief(ticker, n_days, ...)函数是参数驱动的现在我们需要一个能理解自然语言指令的run_query(query, ...)函数。4.1 新的run_query函数接口在copilot.py中我们定义新的入口函数。它的核心任务是解析用户的自然语言查询结合强制参数构造一个给LangChain智能体的、指令明确的提示词Prompt。# 在 copilot.py 中 from typing import Tuple, Dict, Any, List import json from your_agent_module import AGENT, system_prompt # 假设你的智能体定义在其他模块 from .utils import normalize_ticker # 一个用于标准化股票代码的辅助函数 def run_query( query: str, default_ticker: str AAPL.US, default_n_days: int 60, force_fundamentals: bool False, force_risk: bool False, force_news: bool False, news_limit: int 5, ) - Tuple[str, Dict[str, Any]]: 核心查询函数。 参数: query: 用户自然语言指令。 default_*: 当query中未指定时的默认值。 force_*: 是否强制包含某类信息。 news_limit: 强制包含新闻时的数量。 返回: (brief_md, artifacts) brief_md: Markdown格式的简报文本。 artifacts: 字典包含从工具调用中提取的所有原始数据。 # 清理查询字符串 q (query or ).strip() # 如果查询为空使用默认参数构造一个基本查询 if not q: q f为 {default_ticker} 生成过去 {default_n_days} 个交易日的简报。 # 构建给智能体的约束指令列表 constraints [ 约束条件:, 1) 所有数据必须通过工具获取严禁编造数字。, 2) 不要直接倾倒原始价格数据或冗长的新闻列表。, 3) 输出格式为清晰的Markdown建议包含概览、关键指标、市场解读、风险提示等部分。, 4) 保持简洁有用。, f5) 如果查询未指定时间窗口默认使用过去 {default_n_days} 个交易日。, f6) 如果查询未指定股票代码默认使用 {normalize_ticker(default_ticker)}。, ] # 根据强制参数添加额外约束 if force_fundamentals: constraints.append(7) 你必须包含基本面指标市盈率PE、市净率PB、市值、行业、Beta值等。使用fundamentals_snapshot工具。) if force_risk: constraints.append(8) 你必须包含风险指标年化波动率和最大回撤。使用risk_metrics工具时间窗口与回报计算窗口一致。) if force_news: constraints.append(f9) 你必须包含新闻头条。精确获取 {news_limit} 条。使用latest_news工具。) # 组合成最终的提示词 user_prompt 用户查询:\n q \n\n \n.join(constraints) # 调用智能体 response AGENT.invoke( {messages: [(system, system_prompt), (user, user_prompt)]} ) # 处理响应 messages response.get(messages, []) final_msg messages[-1] if messages else None brief_md getattr(final_msg, content, ) or # 关键步骤从智能体消息历史中提取工具调用的原始结果 artifacts _extract_artifacts(messages) return brief_md, artifacts为什么这样设计将解析逻辑从自然语言中提取ticker,n_days和强制逻辑force_*完全放在后端是保持系统行为一致性的关键。无论前端是Streamlit、一个命令行工具还是一个API接口只要调用run_query得到的行为都是一样的。这避免了业务逻辑分散在前后端导致难以维护和调试。4.2 从消息历史中提取工具结果_extract_artifacts这是连接“智能体世界”和“UI世界”的桥梁。LangChain智能体在运行过程中每次调用工具都会在消息历史中留下记录。我们需要遍历这些记录把我们需要的数据价格、基本面、风险、新闻提取出来整理成一个结构化的字典。def _safe_json_loads(content: Any) - Any: 安全地尝试将内容解析为JSON。 if isinstance(content, dict): return content if isinstance(content, str): try: return json.loads(content) except json.JSONDecodeError: return None return None def _extract_artifacts(messages: List[Any]) - Dict[str, Any]: 从LangGraph智能体的消息列表中提取工具调用结果。 根据工具名称的后缀进行匹配。 out: Dict[str, Any] {} for m in messages: # 获取工具调用的名称和内容 name getattr(m, name, None) content getattr(m, content, None) if not name: continue # 尝试解析内容为JSON payload _safe_json_loads(content) if payload is None: continue # 根据工具名称分类存储 if name.endswith(last_n_days_prices): out[price] payload elif name.endswith(fundamentals_snapshot): out[valuation] payload # 使用更通用的键名 elif name.endswith(risk_metrics): out[risk] payload elif name.endswith(latest_news): out[headlines] payload return out实操心得工具命名约定这里假设你的工具名称有特定的后缀如last_n_days_prices。你需要根据自己实际定义的工具体系来调整if判断条件。保持一致的命名规范能让这个提取函数更稳定。错误处理_safe_json_loads函数很重要。工具返回的内容有时可能是字典有时可能是JSON字符串。这个函数确保了两种情况下都能正确解析。数据结构提取出的artifacts字典其键如price,valuation将成为前端渲染函数_render_metrics的输入依据。前后端通过这个字典契约进行通信。5. 前端渲染逻辑构建双面板输出当用户点击“生成简报”按钮后前端需要做两件事1) 调用run_query2) 将返回的结果渲染到页面上。5.1 主程序逻辑连接按钮与后端在app.py的侧边栏代码之后我们添加主显示逻辑。# 在 app.py 中侧边栏代码块之后 if run_btn: # 显示加载指示器提升用户体验 with st.spinner(正在调用工具并生成简报请稍候...): try: brief_md, artifacts run_query( queryquery, default_tickerdefault_ticker, default_n_daysdefault_n_days, force_fundamentalsinclude_fund, force_riskinclude_risk, force_newsinclude_news, news_limitnews_limit, ) except Exception as e: st.error(f生成简报时出错: {e}) st.stop() # 发生错误时停止后续渲染 # 成功获取结果后创建双面板布局 # 使用比例分配宽度左边简报区域稍宽 left_col, right_col st.columns([1.5, 1]) with left_col: st.subheader( 市场简报) # 使用st.markdown渲染AI生成的Markdown内容 st.markdown(brief_md) with right_col: st.subheader( 工具验证指标) # 调用渲染函数展示结构化数据 _render_metrics(artifacts) else: # 初始状态或未点击按钮时显示引导信息 st.info(请在左侧设置输入参数然后点击 **生成简报** 按钮。)关键点st.spinner在调用可能耗时的后端函数时提供一个视觉反馈告诉用户应用正在工作避免用户以为卡顿而重复点击。try...except对后端调用进行基本的错误捕获。网络问题、API限额、智能体逻辑错误都可能导致异常友好的错误提示至关重要。st.columns([1.5, 1])这里定义了左右栏的宽度比例。你可以根据实际内容调整这个比例让布局看起来更平衡。5.2 指标渲染函数_render_metrics这个函数负责将artifacts字典中的原始数据转换成Streamlit上美观、易读的组件。它的设计原则是类型安全、优雅降级、信息清晰。def _render_metrics(artifacts: dict): 渲染工具返回的结构化数据。 处理数据缺失、错误等情况并格式化显示。 # 创建顶部指标的三列布局 metric_cols st.columns(3) price_data artifacts.get(price) valuation_data artifacts.get(valuation) risk_data artifacts.get(risk) headlines_data artifacts.get(headlines) # 第一列价格窗口 with metric_cols[0]: st.subheader( 价格窗口) if isinstance(price_data, dict): if error not in price_data: # 显示核心指标总回报率 total_return_pct price_data.get(total_return, 0.0) * 100 st.metric(总回报率, f{total_return_pct:.2f}%) # 显示时间范围 st.caption(f{price_data.get(start_date, N/A)} 至 {price_data.get(end_date, N/A)} | 天数: {price_data.get(n, N/A)}) # 以表格形式展示更多细节 detail_df pd.DataFrame([price_data])[[first_close, last_close, total_return]].rename( columns{ first_close: 期初收盘价, last_close: 期末收盘价, total_return: 总回报率(小数) } ).T # 转置以便纵向阅读 st.dataframe(detail_df, use_container_widthTrue) else: st.warning(f价格数据错误: {price_data.get(error)}) else: st.info(未请求或未使用价格工具。) # 第二列基本面指标 with metric_cols[1]: st.subheader( 基本面) if isinstance(valuation_data, dict): if error not in valuation_data: df pd.DataFrame([valuation_data]) # 定义我们想展示的关键字段 key_columns [ticker, name, sector, market_cap, pe, pb, beta, dividend_yield, profit_margin] # 只保留数据中存在的列 existing_cols [col for col in key_columns if col in df.columns] if existing_cols: # 转置DataFrame让指标名称在一列数值在另一列更易于阅读 display_df df[existing_cols].T display_df.columns [数值] # 为转置后的单列命名 st.dataframe(display_df, use_container_widthTrue) else: st.info(基本面数据中未找到标准字段。) else: st.warning(f基本面数据错误: {valuation_data.get(error)}) else: st.info(未请求或未使用基本面工具。) # 第三列风险指标 with metric_cols[2]: st.subheader(⚠️ 风险指标) if isinstance(risk_data, dict): if error not in risk_data: vol_pct risk_data.get(volatility_ann, 0.0) * 100 drawdown_pct risk_data.get(max_drawdown, 0.0) * 100 st.metric(年化波动率, f{vol_pct:.2f}%) st.metric(最大回撤, f{drawdown_pct:.2f}%) st.caption(f{risk_data.get(start_date, N/A)} 至 {risk_data.get(end_date, N/A)} | 天数: {risk_data.get(n, N/A)}) # 可选展示更多风险细节 # other_risk_details {k: v for k, v in risk_data.items() if k not in [volatility_ann, max_drawdown, start_date, end_date, n]} # if other_risk_details: # st.dataframe(pd.DataFrame([other_risk_details]).T, use_container_widthTrue) else: st.warning(f风险数据错误: {risk_data.get(error)}) else: st.info(未请求或未使用风险工具。) # 新闻头条部分独占一行 st.subheader( 最新头条) if isinstance(headlines_data, list) and len(headlines_data) 0: for i, headline in enumerate(headlines_data, 1): title headline.get(title, 无标题) link headline.get(link, ) source headline.get(source, ) date headline.get(date, ) # 格式化显示 line f{i}. **{title}** if source: line f *({source})* if date: line f - {date} # 如果有链接使其可点击 if link: st.markdown(f{line} \n 链接: {link}) else: st.markdown(line) else: st.info(未请求或未使用新闻工具或未获取到新闻。)渲染逻辑的精髓防御性编程每个数据块都先用isinstance检查类型再用error in data检查工具调用是否成功。这能优雅地处理API失败、数据缺失等情况给用户明确的反馈st.warning,st.info而不是抛出令人困惑的异常。数据格式化百分比将小数形式的回报率、波动率乘以100并格式化为%.2f%%符合阅读习惯。表格转置对于基本面这类键值对数据将DataFrame转置.T后显示使得指标名称和数值纵向排列更易于扫描和比较。日期与说明使用st.caption添加细微的说明文字如时间范围、数据点数提供上下文。用户体验st.metric用于展示最关键的一两个数字如总回报、波动率Streamlit会将其渲染得特别突出并配有视觉上的“卡片”效果。st.dataframe用于展示稍复杂的数据表并设置use_container_widthTrue让其自适应宽度。新闻列表将新闻渲染为带编号的列表并高亮标题。如果存在来源和日期以较小的字体显示在旁边信息层次清晰。6. 实际应用演示与场景分析让我们通过几个具体的查询示例来看看这个MVP如何应对不同的工作场景。这些场景来源于真实的金融分析需求。6.1 场景一标准简报回报估值新闻查询指令“为 AAPL.US 生成过去60个交易日的简报。包含PE和PB并获取5条最新头条新闻。”用户意图快速了解一家公司近期的整体市场表现、估值水平和市场情绪。系统行为解析出ticker“AAPL.US”,n_days60。由于查询明确要求PE、PB和新闻后台会调用fundamentals_snapshot和latest_news工具。智能体生成简报总结价格走势、解读估值数据、并提炼新闻要点。UI左侧显示这份综合简报右侧面板则分栏展示具体的总回报率、PE/PB数值、以及5条新闻的标题和链接。输出价值一份立即可用于团队晨会或内部备忘录的完整摘要所有关键数据点触手可及。6.2 场景二风险聚焦工作流查询指令“分析 MSFT.US 过去90个交易日的风险状况。计算年化波动率和最大回撤。保持简洁不要新闻。”用户意图评估特定时间段内资产的价格波动风险和潜在损失幅度常用于风险报告或投资组合回顾。系统行为解析出ticker“MSFT.US”,n_days90并识别出“不要新闻”的指令。强制调用risk_metrics工具即使用户没提“风险”二字引擎也应从“风险状况”推断出。简报内容将聚焦于波动性描述、回撤事件分析及其可能原因。UI右侧面板的风险指标列将高亮显示而新闻板块则显示“未请求”的信息提示。输出价值一份专注的风险快照帮助理解在市场压力时期该资产的表现。6.3 场景三纯新闻主题扫描查询指令“获取 NVDA.US 最近7条头条新闻。用6到8行话总结市场关注点的变化。提及主题不要罗列每一条新闻。除非必要否则不要计算回报。”用户意图快速捕捉市场叙事Narrative的转变了解当前驱动股价和情绪的主要话题是什么。系统行为解析出ticker“NVDA.US”,news_limit7并理解“总结”、“主题”等指令。主要调用latest_news工具。智能体的挑战在于进行“摘要的摘要”——它需要阅读7条新闻识别共同主题如“AI芯片需求”、“供应链更新”、“竞争对手动态”并生成高度凝练的叙述。简报将是一段纯粹的主题性文字。右侧面板则整齐列出7条新闻的原始标题和来源供用户追溯和深度阅读。输出价值在信息过载的时代快速提供市场情绪的“温度计”和话题雷达。7. 生产环境注意事项与进阶优化将这个MVP推向真实团队使用你会立刻遇到一些在演示中不会出现的问题。提前规划能节省大量后期调试时间。7.1 预料之中的“坑”与应对策略混乱的股票代码输入用户会输入aapl、AAPL、AAPL.US甚至Apple。如果后端API只接受特定格式应用就会崩溃。解决方案实现一个健壮的normalize_ticker()函数。它可以处理大小写转换、添加默认后缀如.US、甚至维护一个公司名到代码的简单映射表。在run_query函数内部尽早调用这个函数对解析出的或默认的股票代码进行标准化。API数据缺失与错误不是所有股票都有完整的新闻数据某些市场的基本面字段可能为null对于非常短或非常古早的时间窗口价格API可能返回空值。解决方案确保你的工具函数如get_fundamentals,get_news都有完善的错误处理返回结构化的错误信息例如{“error”: “No news found for ticker XYZ”}而不是抛出异常。正如我们在_render_metrics函数中做的UI层需要能优雅地显示这些错误而不是崩溃。成本与性能eod_prices获取每日价格工具如果被滥用例如请求长达10年的数据会显著增加API调用成本和响应时间。策略在工具设计层面进行限制。例如last_n_days_prices工具内部应设定一个最大天数上限如500天。优先使用像60-day summary这样的聚合工具它只返回开始价、结束价和总回报数据量小速度快。仅在明确需要每日数据时才使用完整价格工具。输出格式漂移LLM智能体有时会“放飞自我”不遵守你设定的Markdown格式或者添加无关的解释。策略保持提示词Prompt的严格和简洁。明确的约束列表如“输出格式为清晰的Markdown包含概览、关键指标、市场解读、风险提示”比模糊的指令更有效。定期用测试用例验证输出格式。7.2 适合此MVP的简单扩展方向当你的MVP得到初步验证后可以考虑以下低成本、高价值的功能扩展多股票对比这是最自然的需求延伸。修改run_query函数使其能接受一个股票代码列表。在UI上可以增加一个“添加对比股票”的按钮。后端逻辑需要为每个股票并行或顺序运行智能体然后生成一个对比性的简报例如“过去一个月AAPL上涨5%而MSFT下跌2%主要差异在于...”右侧面板可以用并排的表格展示各股票指标。简报订阅与推送许多分析是周期性的。你可以构建一个简单的调度系统例如使用schedule库或Celery每天/每周自动为预设的关注列表Watchlist运行查询然后将生成的简报Markdown通过Slack Webhook或电子邮件发送给订阅者。这立刻将工具从“交互式查询”升级为“自动化报告服务”。结果缓存在演示或团队内部高频查询时反复请求相同股票、相同时间窗口的数据是浪费。实现使用functools.lru_cache装饰器缓存run_query函数的结果缓存键可以是(query, default_ticker, default_n_days, ...)参数的哈希值。更精细的缓存可以放在工具层例如缓存(ticker, n_days)的价格数据。这能极大提升UI响应速度并节省API调用。后端服务化如果你希望其他内部系统如数据分析平台、内部仪表盘也能使用这个简报生成能力那么将run_query函数封装成一个FastAPI或Flask端点就是下一步。你的Streamlit应用可以变成这个API的一个前端消费者而其他应用也可以直接调用API获取JSON格式的简报和指标数据。走到这一步你已经拥有了一个功能完整、架构清晰的市场简报副驾驶MVP。它的价值不在于使用了多么复杂的LLM模型而在于它创造了一个可重复的工作流并将散落在各处的数据价格、基本面、新闻和智能LLM解读整合成了一个可供非技术人员直接消费的产品界面。这个界面不仅展示了AI的“思考结果”更重要的是通过右侧的“工具验证指标”它建立了透明度与信任。接下来最好的步骤就是把它交给真实用户——你的产品经理、分析师或销售同事——让他们用真实的问题去使用一周。他们的反馈会清晰地告诉你接下来应该投资于缓存优化、多股票对比还是接入新的数据源。这个MVP就是你探索真实需求的最佳罗盘。