copilot.py文件中,该模块通过基于EODHD的工具来获取所需数据,这些工具其实就是一些Python函数,它们会调用EODHD提供的接口并返回可预测的结果。app.py文件中的Streamlit应用程序只是一个外壳,它的作用仅仅是调用核心处理模块,并将分析结果与相关数据指标一起呈现出来。

需要特别说明的一点:在本手册中,当我提到“copilot”时,我指的并不是GitHub上的Copilot插件。这里所说的“copilot”是一个内置在产品中的辅助系统,它通过调用各种数据工具并从这些工具的输出中生成有用的市场分析信息。

先决条件与所需工具

  • 已安装Python 3.10或更高版本

  • 拥有一个EODHD API密钥

  • 拥有一个OpenAI API密钥

  • 拥有一个可正常使用的本地开发环境(venv或conda都可以)

  • EODHD。你会使用它来获取当日收盘价、基本财务数据以及新闻信息。

  • OpenAI。你会利用其聊天模型来生成分析报告,但前提是这些模型必须先获得所需的数据支持。

  • LangChain + LangGraph。你会使用这些工具以及ReAct风格的智能体,让模型自行决定该调用哪些数据函数,从而生成简洁的分析报告。

  • Streamlit。你只会用它来快速演示这个辅助系统作为可交互式产品界面时的功能。

目录结构

MVP的功能

从本质上讲,这个MVP只有一个任务:将一个常规问题转化为一份简短且可重复使用的市场分析报告。

你需要提供以下信息:

  • 一只股票代码(例如AAPL.US)

  • 指定的交易天数范围(例如60天或120天)

  • 你真正想要了解的信息

  • 可选参数,用于确保报告中包含某些特定内容,如基本数据、风险分析或新闻标题

在实际操作中,这些信息会决定报告的具体内容。而可选参数的存在则是为了保证团队能够获得标准格式的报告。

最终,该系统会返回两样结果:

  1. 一份结构清晰的Markdown格式报告,便于快速阅读

  2. 一组由工具生成的原始数据,这些数据可以直接在用户界面中展示,无需再次调用API

第二份输出结果非常重要。它能使应用程序运行得更加高效,并确保所有数据的真实性。

不可妥协的原则

这个MVP的设计初衷是作为一个完整的产品功能,而不是一个简单的演示工具。

  • 所有数据分析都基于工具提供的原始信息,系统不会进行主观猜测

  • 如果缺少某些数据,系统会明确指出这一点

  • 不会提供未经处理的原始价格数据,也不会列出大量的新闻标题

  • 系统只会计算用户请求的具体内容

  • 生成的报告格式类似于内部备忘录或Slack消息

一旦这种工作模式建立起来,就会带来许多好处。

首先,各个部门都可以重复使用这些统一格式的报告;同时,编写每周的市场分析报告也会变得更加快捷。演示过程也变得非常简单:只需输入查询参数,就能立即得到报告,并查看相应的数据指标。

系统架构

我们的架构设计得非常简洁:只有两个文件,各自承担明确的职责。

copilot.py – 核心执行模块

这个文件包含了让整个系统正常运行的所有关键组件:

  • 用于获取价格、基本数据、新闻和风险信息的工具

  • 代理程序的配置规则及提示机制

  • 一个名为run_brief()的函数,它接收输入参数并生成以下结果:

  • Markdown格式的报告文本

  • 供用户界面使用的结构化数据

如果你以后想在其他地方使用这个系统,这个文件就是你需要保留的关键文件。

app.py – MVP外壳程序

这只是一个基于Streamlit的工具层:

  • 用于输入股票代码、时间范围、查询参数等信息的界面元素

  • 双栏布局:左侧显示报告内容,右侧展示数据指标和新闻标题

这个程序本身不包含任何数据处理逻辑,它只是调用run_brief()函数并显示结果而已。

为何这种分离设计如此重要

如果将所有代码混合放在一个Streamlit脚本中,以后就再也无法使用其他技术来替换Streamlit了。而采用这种分离结构,你日后可以将FastAPI替换掉Streamlit,而无需重新编写核心逻辑。此外,“产品逻辑”被集中保存在一个地方,这使得测试和迭代工作变得更加方便。同时,也能避免出现UI代码和数据代码难以维护的问题。

copilot.py:构建后端引擎

在本节中,我们将构建后端引擎。完成本节的编写后,你将会得到一个可执行的函数,该函数接收查询请求,使用EODHD工具获取所需数据,并返回两样结果:一份适合人类阅读的Markdown格式摘要,以及一份供用户界面使用的结构化数据字典。

1. 导入包

我们尽量简化代码结构。这样做的目的并不是为了炫耀所使用的工具,而是为了编写出功能完备且易于维护的程序。


import json
from datetime import datetime, timedelta
from typing import Any, Dict, List, Optional, Tuple
import numpy as np
import pandas as pd
import requests

from langchain_openai import ChatOpenAI
from langchain_core.tools import tool
from langgraph.prebuilt import create_react_agent

eodhd_api_key = '你的EODHD API密钥'
openai_api_key = '你的OPENAI API密钥'

除了导入这些包之外,我们还在文件开头定义了eodhd_api_keyopenai_api_key,这样程序就可以直接运行了。在实际部署环境中,这些配置信息通常会被存储在环境变量中。

2. 辅助函数

在开始使用各种工具或构建智能体之前,我们先编写三个辅助函数。虽然这些函数与“人工智能”技术没有直接关系,但它们对于确保程序的稳定运行却至关重要。


def normalize_ticker(t: str) -> str:
    t = (t or "").strip().upper()
    if not t:
        return t
    if "." in t:
        return t
    return f"{t}.US"

def _safe_json_loads(x: Any) -> Optional[Any]:
    if x is None:
        return None
    if isinstance(x, (dict, list)):
        return x
    if not isinstance(x, str):
        return None
    try:
        return json.loads(x)
    except Exception:
        return None

def get_eod_prices_raw(ticker: str, start: str, end: str) -> pd.DataFrame:
    url = f"https://eodhd.com/api/eod/{ticker}"
    params = {"from": start, "to": end, "api_token": eodhd_api_key, "fmt": "json"}
    r = requests.get(url, params=params)
    data = r.json()

    if not isinstance(data, list) or not data:
        return pd.DataFrame(columns=["date", "open", "high", "low", "close", "volume", "ticker"])

    df = pd.DataFrame(data)
    keep = [c for c in ["date", "open", "high", "low", "close", "volume"] if c in df.columns]
    df = df[keep].copy()
    df["ticker"] = ticker
    df["date"] = pd.to_datetime(df["date"], errors="coerce")
    df = df.dropna(subset=["date", "close")).sort_values("date").reset_index(drop=True)
    return df

下面是对这三个辅助函数的简要说明:

  • normalize_ticker()用于规范用户输入。人们有时会输入aapl、AAPL,或者加上空格输入AAPL.US这样的格式。而EODHD要求输入的符号格式必须统一,因此这个函数会在进行任何API调用之前确保输入格式的一致性。

  • _safe_json_loads()的作用是处理从智能体返回的数据。这些数据可能是Python字典或列表,也可能是JSON字符串。这个辅助函数能够确保我们能够正确地解析这些数据而不会引发错误。

  • get_eod_prices_raw()是一个基础价格获取函数。所有需要OHLCV数据的工具都会使用这个函数,而不需要每次都重新编写请求逻辑或进行数据处理。该函数会使用EODHD的日终历史数据API获取数据,然后对数据进行清洗和处理,确保返回的数据格式统一且没有缺失值,这样其他工具就可以直接使用这些数据了。

就是这么简单。没有任何复杂的设计,这样做只是为了让其他代码的运行逻辑更加容易预测而已。

3. 数据工具

在构建这个系统之前,我们首先需要一个可靠的数据层。

如果将这个项目开发成一款产品,那么这些工具就相当于你的“内部API”——它们决定了辅助系统能做什么、不能做什么。辅助系统只是调用这些工具,并将它们的输出结果整理成简洁的信息而已。

在这个最小可行产品版本中,每个工具都负责特定的功能,并且会返回结构紧凑的输出结果。这是有意为之的。你希望用户界面中的信息呈现方式具有可预测性;同时,你也应该尽量避免将原始数据直接输入到模型中,除非确实有这样的需求。


@tool
def last_n_days_prices(ticker: str, n: int = 60) -> Dict[str, Any]:
    """
    提供过去N个交易日的价格信息。
    返回的是结构简洁的摘要数据,不包含原始数据行。
    """
    ticker = normalize_ticker(ticker)

    end = datetime.utcnow().date().isoformat()
    start = (datetime.utcnow().date() - timedelta(days=240)).isoformat()

    df = get_eod_prices_raw(ticker, start, end)
    if df.empty:
        return {"ticker": ticker, "error": "no_price_data"}

    df = df.tail(int(n)).reset_index(drop=True)
    if df.empty:
        return {"ticker": ticker, "error": "no_price_data"}

    first_close = float(df.loc[0, "close"])
    last_close = float(df.loc[len(df) - 1, "close"])
    total_return = float((last_close / first_close) - 1.0)

    return {
        "ticker": ticker,
        "n": int(n),
        "start_date": str(df.loc[0, "date"].date()),
        "end_date": str(df.loc[len(df) - 1, "date"].date()),
        "first_close": first_close,
        "last_close": last_close,
        "total_return": total_return,
    }

@tool
def fundamentals_snapshot(ticker: str) -> Dict[str, Any]:
    """
    提供该股票的基本面信息摘要。
    返回的是一个扁平化的字典结构。
    """
    ticker = normalize_ticker(ticker)

    url = f"https://eodhd.com/api/fundamentals/{ticker}"
    params = {"api_token": eodhd_api_key, "fmt": "json"}
    r = requests.get(url, params=params)
    data = r.json()

    if not isinstance(data, dict) or not data:
        return {"ticker": ticker, "error": "no_data"}

    highlights = data.get("Highlights", {}) or {}
    general = data.get("General", {}) or {}
    valuation = data.get("Valuation", "") or {}
    technicals = data.get("Technicals",**) or {}

    return {
        "ticker": ticker,
        "name": general.get("Name"),
        "sector": general.get("Sector"),
        "industry": general.get("Industry"),
        "market_cap": highlights.get("MarketCapitalization"),
        "pe": valuation.get("TrailingPE"),
        "pb": valuation.get("PriceBookMRQ"),
        "profit_margin": highlights.get("ProfitMargin"),
        "dividend_yield": highlights.get("DividendYield"),
        "beta": technicals.get("Beta"),
    }

@tool
def latest_news(ticker: str, limit: int = 5) -> List[Dict[str, Any]]:
    """
    提供该股票的最新新闻标题。
    返回的是一个包含新闻摘要的字典列表。
    """
    ticker = normalize_ticker(ticker)

    url = f"https://eodhd.com/api/news"
    params = {"s": ticker, "limit": int(limit), "offset": 0, "api_token": eodhd_api_key, "fmt": "json"}
    r = requests.get(url, params=params)
    data = r.json()

    if not isinstance(data, list) or not data:
        return []

    df = pd.DataFrame(data)
    keep = [c for c in ["date", "title", "link", "source"] if c in df.columns]
    df = df[keep].copy()

    if "date" in df.columns:
        df["date"] = pd.to_datetime(df["date"], errors="coerce")
        df = df.sort_values("date", ascending=False)

    out = df.head(int(limit)).reset_index(drop=True).to_dict(orient="records")
    for row in out:
        dt = row.get("date")
        if isinstance(dt, (pd Timestamp, datetime)):
            row["date"] = dt.isoformat()
    return out

@tool
def risk_metrics(ticker: str, start: str, end: str) -> Dict[str, Any]:
    """
    计算指定时间窗口内该股票的风险指标。
    volatility_ann:基于日收益率计算得出的年化波动率
    max_drawdown:该时间窗口内的最大回撤幅度
    """
    ticker = normalize_ticker(ticker)

    df = get_eod_prices_raw(ticker, start, end)
    if df.empty:
        return {"ticker": ticker, "error": "no_price_data"}

    df = df.sort_values("date").reset_index(drop=True)
    df["ret"] = df["close"].pct_change().fillna(0.0)

    vol_ann = float(df["ret"].std(ddof=0) * np.sqrt(252))

    cummax = df["close"].cummax()
    dd = (df["close"] / cummax) - 1.0
    max_dd = float(dd.min())

    first_close = float(df.loc[0, "close"])
    last_close = float(df.loc[len(df) - 1, "close"])
    total_return = float((last_close / first_close) - 1.0)

    return {
        "ticker": ticker,
        "start_date": str(df.loc[0, "date"].date()),
        "end_date": str(df.loc[len(df) - 1, "date"].date()),
        "n": int(len(df)),
        "total_return": total_return,
        "volatility_ann": vol_ann,
        "max_drawdown": max_dd,
    }

@tool
def eod_prices(ticker: str, start: str, end: str) -> List[Dict[str, Any]]:
    """
    提供该股票的原始OHLCV数据行。
    仅适用于那些需要使用其他工具无法完成的自定义计算场景。
    """
    ticker = normalize_ticker(ticker)
    df = get_eod_prices_raw(ticker, start, end)
    return json.loads(df.to_json(orient="records"))

让我们来了解一下这段代码中的关键部分。

1. `last_n_days_prices` – 价格数据窗口

大多数实际查询的起点都是类似“最近发生了什么?”这样的问题。

因此,这个工具的作用就是:它会获取足够多的每日数据条目,以确保能够覆盖过去N个交易日的数据(通过使用缓冲区来处理数据),然后返回一份简洁的总结信息:

  • 数据窗口的起始日期和结束日期

  • 该时间段内的最高价和最低价

  • 总收益

  • 实际使用的数据交易天数

它不会返回原始数据行。这样既能避免输出信息过多,也能保证用户界面的响应速度。

2. `fundamentals_snapshot` – 基本面数据概览

这个工具用于提供快速的基本面信息。在撰写简报时,你通常需要一个大致的估值参考,但又不希望将MVP开发成一个完整的基本面分析系统。

因此,我们会保持它的简洁性。它会一次性调用EODHD的基本面数据API,然后提取出在简报中常用的几项信息:

  • 市盈率、市净率

  • 市值

  • 所属行业及贝塔系数

  • 还有一些可选的额外信息,比如股息收益率和利润率

如果某个字段缺失,它就会返回`None`,而不会进行任何猜测或处理。

3. `latest_news` – 最新新闻标题

如果没有相关的背景信息,单纯的价格变动是毫无意义的。

这个工具会通过EODHD金融新闻API获取某个股票的最新新闻标题,按照发布时间对它们进行排序,然后返回一份只包含我们应用程序真正需要的信息的简洁列表:

  • 发布日期

  • 新闻标题

  • 链接地址

  • 我们在这里并不分析市场情绪。这样做只是为了让简报的内容具有更真实的背景依据。

    4. `risk_metrics` – 风险指标

    有时候,人们关心的不是“发生了什么”,而是“这种价格变动有多剧烈?”

    这时,波动率和中值回撤率就派上用场了。这个工具会根据用户指定的起始日期和结束日期,获取该时间段内的每日收盘价,然后计算出以下指标:

    • 基于每日收益计算出的年化波动率

    • 该时间段内的最大中值回撤幅度

    • 同时,它还会再次为同一时间段计算总收益,以确保所有数据的一致性。

      5. `eod_prices` – 基础数据支持

      这个工具是为将来可能的扩展功能预留的。

      在大多数情况下,MVP版本并不需要原始的OHLCV数据条目。但一旦你需要自定义指标(比如滚动指标、平均真实幅度、自定义信号或模式识别等功能),你就必须使用原始数据了。

      因此,`eod_prices`会以字典列表的形式返回完整的每日数据。

      原则很简单:除非真的有必要,否则不要调用这个功能。因为它会占用更多的系统资源,也最容易导致令牌使用量激增或应用程序运行速度变慢。

      4. 测试数据工具(在copilot.py之外进行)

      在代理程序开始执行任何操作之前,你需要确认数据层能够正常工作。这项测试并非为了娱乐,而是一种快速的验证手段,旨在回答以下三个问题:

      1. 我们能否顺利获取普通股票代码对应的数据而不会出现错误?

      2. 我们所依赖的那些字段确实存在吗?

      3. 输出结果看起来是否合理?否则撰写报告时得到的数据就会毫无意义。

      以下是我实际运行的测试代码。每个工具只被调用一次。我仅打印了关键部分,因此代码量保持得比较简洁。

      
      print("\n--- 过去N天股价数据 ---")
      out_price = last_n_days_prices.invoke({"ticker": "AAPL.US", "n": 60})
      print(out_price)
      
      print("\n--- 基本面数据快照 ---")
      out_fund = fundamentals_snapshot.invoke({"ticker": "AAPL.US"})
      print(out_fund)
      
      print("\n--- 最新新闻 ---")
      out_news = latest_news.invoke({"ticker": "AAPL.US", "limit": 5})
      print(f"新闻条目数量: {len(out_news)}")
      print(out_news[:2])
      
      print("\n--- 风险指标 ---")
      end = datetime.utcnow().date()
      start = (end - timedelta(days=180)).isoformat()
      out_risk = risk_metrics.invoke({"ticker": "AAPL.US", "start": start, "end": end})
      print(out_risk)
      
      print("\n--- 收盘价数据(原始记录,小时间窗口) ---")
      raw_rows = eod_prices.invoke({"ticker": "AAPL.US", "start": "2025-12-01", "end": "2026-01-15"})
      print(f"记录条目数量: {len(raw_rows)}")
      print(raw_rows[:2])
      

      输出结果

      这些输出结果基本上证明了数据层是正常工作的。

      last_n_days_prices提供了过去60个交易日的完整数据:起始收盘价为269.0,结束收盘价为248.04,总回报率为-7.79%。fundamentals_snapshot也返回了撰写报告所需的关键信息,如市盈率33.2048、市净率49.4443、市值约为3.665万亿,以及所属行业和板块。

      latest_news返回了5条格式统一的新闻记录,包括日期、标题和链接。risk_metrics同样能够正常工作,但它使用的是不同的时间窗口(过去180个日历天数对应的是123个交易天数),因此其计算出的总回报率为+18.65%,这与60天时间窗口得出的结果不一致。后来我们强制要求risk_metrics使用与last_n_days_prices相同的起止日期。

      eod_prices按预期返回了32条原始数据记录。这里的日期字段以纪元格式显示,这是合理的,因为这个工具主要是用于内部计算,并非直接展示给用户。

      5. 创建代理程序

      在这个阶段,所有这些组件才会真正组合成一个完整的代理系统,而不再是一堆独立的函数。我们需要定义代理程序的行为规则,指定它只能使用哪些工具,同时还要建立一种机制,以便将工具的输出结果传递给用户界面进行展示。

      
      system_prompt = (
          "你是一个嵌入在产品中的市场分析辅助工具。\n"
          "规则如下:\n"
          "1) 使用现有工具获取数据,切勿自行编造数字。\n"
          "2) 不要直接输出原始的价格数据或冗长的新闻列表。\n"
          "3) 如果用户没有提出具体要求,就不要进行相关计算。\n"
          "4) 以清晰的Markdown格式输出信息,并使用分节结构。\n"
          "5) 保持内容简短实用,就像内部仪表盘上的通知一样。\n"
          "工具使用指南:\n"
          "- 使用`last_n_days_prices`获取最近几天的价格数据。\n"
          "- 使用`fundamentals_snapshot`获取基本财务数据、市盈率、市净率、市值等信息。\n"
          "- 使用`latest_news`获取最新新闻标题。\n"
          "- 只有在用户询问波动率或回撤率时,才使用`risk_metrics`。\n"
          "- 仅在需要自定义计算时,才使用`eod_prices`。\n"
      )
      
      def _build_agent() -> Any:
          llm = ChatOpenAI(
              model='gpt-5-nano',
              temperature=0,
              api_key=openai_api_key,
          )
          tools = [last_n_days_prices, fundamentals_snapshot, latest_news, risk_metrics, eod_prices]
          return create_react_agent(model=llm, tools=tools)
      
      AGENT = _build_agent()
      
      def _extract_artifacts(messages: List[Any]) -> Dict[str, Any]:
          """
          从LangGraph消息列表中提取工具生成的输出结果。
          这样可以避免在Streamlit中多次调用相同接口。\n"
          """
          out: Dict[str, Any] = {}
          for m in messages:
              name = getattr(m, "name", None)
              content = getattr(m, "content", None)
      
              if not name:
                  continue
      
              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
      

      系统提示实际上就相当于一份契约。如果你不明确指定这些要求,代理程序最终就会偏离正确的方向——它可能会开始随意猜测数字、生成冗长的输出内容,或者去做那些你根本没有要求它做的事情。而这个明确的提示机制能确保代理程序始终按照预定的路径运行,工具的使用指南也能有效避免误用。

      _build_agent()这个函数主要是负责连接各个组件:一个模型、一套固定的工具,以及一个能够决定在何时使用哪些工具的ReAct代理程序。另一个关键部分是_extract_artifacts()——我们开发这个函数的目的并不仅仅是为了生成一段美观的文字,而是为了让系统产生结构化的数据,这样用户界面才能顺利地展示这些信息。因此,在Streamlit中我们并没有再次调用那些接口,而是直接使用了在代理程序运行过程中已经产生的结果。

      6. 将代理程序转化为可被应用程序调用的后端服务

      到目前为止,我们已经构建好了工具和代理程序,但接下来需要做的就是将它们转化成应用程序可以像调用普通后端函数一样来使用的形式。输入一个参数,就能得到一份简报以及渲染用户界面所需的结构化数据。

      
      def run_brief(
          ticker: str,
          n_days: int = 60,
          include_fundamentals: bool = True,
          include_risk: bool = False,
          include_news: bool = True,
          news_limit: int = 5,
      ) -> Tuple[str, Dict[str, Any]]:
          """
          返回结果:
            - 一份Markdown格式的简报
            - 一个包含各种数据的字典,其中键值为价格、估值、风险系数、头条新闻等(这些数据是在使用相关工具时产生的)
          """
          t = normalize_ticker(ticker)
      
          request_parts = [
              f"股票代码:{t}.",
              f"计算过去{int(n_days)}个交易日的总收益。",
          ]
          if include_fundamentals:
              request_parts.append("获取基本财务数据,并报告市盈率、市净率、市值、所属行业以及贝塔系数。")
          if include_risk:
              request_parts.append("计算同一时间窗口内的年化波动率和最大回撤幅度。」
              request_parts.append("使用与收益计算相同的起始日期和结束日期。")
          if include_news:
              request_parts.append(f"获取最近{int(news_limit)}条头条新闻,并对其进行简要介绍。")
          request_parts.append(
              "撰写一份简短的市场分析报告,内容包括市场概况、各项指标、可能的影响以及需要注意的事项。"
          )
          request_parts.append("保持简洁,不要复制原始数据。")
      
          userprompt = " ".join(request_parts)
      
          response = AGENT.invoke(
              {"messages": [("system", system.prompt), ("user", user_prompt)]}
          )
      
          messages = response.get("messages", [])
          final_msg = messages[-1]
          brief_md = getattr(final_msg, "content", "") or ""
      
          artifacts = _extract_artifacts(messages)
          return brief_md, artifacts
      

      run_brief函数承担了两项任务。首先,它将“用户的需求”转化为一系列具体的指令,从而确保代理程序能够按照预定的路径执行操作。这就是为什么它会先生成request_parts列表,而不是直接给用户一个空白的提示框然后听天由命。

      其次,这个函数会返回两个结果:brief_md用于在应用程序的左侧显示,而artifacts则用于在右侧展示。这些数据实际上都是通过_extract_artifacts(messages)函数获取的——这种方式能够有效地重用在代理程序运行过程中已经产生的结果,从而避免为了填充用户界面而再次调用相关功能。

      演示运行示例(在copilot.py文件外部执行)

      以下是三个示范案例,这些案例展示了产品经理、创始人或分析师在实际使用该工具时的操作方式。每个演示都包含了简短的设置步骤、实际运行的代码、输出结果,以及针对输出内容的详细解读。

      演示1:基础信息查询示例

      这是最常见的“提供当前情况概览”的请求类型。在输出结果中,你会看到一个窗口界面、相应的回答内容、关键财务数据,以及一段简短的标题说明。

      
      def run_agent(query: str):
          resp = AGENT.invoke({"messages": [("system", system_prompt), ("user", query)]})
          msgs = resp.get("messages", [])
          final = msgs[-1].content if msgs else ""
          print("\n" + "=" * 80)
          print("查询内容:")
          print(query)
          print("\n回答结果:")
          print(final)
          return resp
      
      resp = run_agent(
          "股票代码:AAPL.US。计算过去60个交易日的总回报率;获取该股票的财务基本数据,包括市盈率、市净率、市值、所属行业及贝塔系数;检索最近5条相关新闻并简要介绍;撰写一份简短的市场分析报告,内容包括现状概述、关键指标、可能意味着什么以及需要注意的事项。请保持内容简洁,不要直接粘贴原始数据行。"
      )
      

      输出结果:

      
      ================================================================================
      查询内容:
      股票代码:AAPL.US。计算过去60个交易日的总回报率;获取该股票的财务基本数据,包括市盈率、市净率、市值、所属行业及贝塔系数;检索最近5条相关新闻并简要介绍;撰写一份简短的市场分析报告,内容包括现状概述、关键指标、可能意味着什么以及需要注意的事项。请保持内容简洁,不要直接粘贴原始数据行。
      
      回答结果:
      ### 现状概述
      - 时间范围:过去60个交易日(2025-10-28至2026-01-23)
      - 价格走势:269.00 → 248.04
      - 总回报率:-7.79%
      
      ### 关键财务数据
      - 行业:科技行业
      - 市值:3.665万亿美元(3,665,126,490,112)
      -市盈率:33.20
      -市净率:49.44
      -贝塔系数:1.09
      
      ### 可能意味着什么
      - 在科技行业整体表现不佳的背景下,该股票的过去60个交易日总回报率出现了约7.8%的下降。较高的市净率可能反映出投资者对其资产负债表状况或未来增长潜力的较高预期。近期的一些宏观新闻因素也在影响该股票的市场走势。
      
      ### 相关新闻链接
      - IWO vs. MGK:小盘股多元化投资与大盘股成长投资的对比——关于投资策略选择的话题
      - 随着政府关门危机的加剧,美联储会议前的股市期货价格正在下跌——宏观风险因素对市场的影响
      - 这位创始人曾从事消防工作,现在他正在开发一个人工智能相关的项目——人工智能/科技领域的最新发展动态
      - 道琼斯期货指数下跌;特朗普的关税政策、政府关门危机以及企业财报成为当前市场的关注焦点——财报与宏观经济形势对股市的影响
      - SPDR的SPTM指数覆盖了广泛的市场范围,而Vanguard的VTV指数则更侧重于价值股投资。哪一只基金更适合投资者购买?——关于市场广度与价值投资的讨论
      
      ### 注意事项
      - 上述数据反映的是最新的情况;后续的数据更新可能会影响总回报率、倍数指标以及财务基本数据。
      - 这里的市净率数值相对较高,这可能反映了市场对该公司资产价值的看法,而并非完全基于其盈利能力的评估结果。
      - 本内容并不构成投资建议,请您结合更全面的背景信息以及自身的风险承受能力来做出决策。

      核心数据在于在明确指定的时间区间内(2025年10月28日至2026年1月23日),该指标出现了-7.79%的跌幅。 “各项指标”部分正好符合我们编写内部报告时的需求:它提供了行业分类以及简洁的估值信息(市盈率、市净率、贝塔系数、市值等),但并不会变成一份详尽的基本面分析报告。

      新闻标题被刻意写得简短,且更侧重于宏观层面的内容。如果你只需要了解市场当前的总体趋势,而不是深入了解仅与苹果公司相关的具体消息,那么这样的标题设计非常实用。

      演示2:以风险分析为主的简要报告(在同一时间区间内分析波动性及最大跌幅)

      这种报告形式的目的就是让读者能够快速了解情况:“告诉我情况到底有多糟糕”。报告结果应该会显示在同一时间区间内计算得出的回报率、波动性以及最大跌幅这些数据。

      
      resp = run_agent(
          "Ticker: MSFT.US. 计算过去90个交易日的总回报率。  
          计算同一时间区间内的年化波动性及最大跌幅。  
          使用与回报率计算相同的起始日期和结束日期。  
          编写一份简短的市场分析报告,内容包括:市场概况、各项指标、可能意味着什么以及需要注意的事项。"
      )
      

      输出结果:

      
      ================================================================================
      查询请求:
      Ticker: MSFT.US. 计算过去90个交易日的总回报率。计算同一时间区间内的年化波动性及最大跌幅。使用与回报率计算相同的起始日期和结束日期。编写一份简短的市场分析报告,内容包括:市场概况、各项指标、可能意味着什么以及需要注意的事项。
      
      回复结果:
      ## 市场分析报告 - MSFT.US
      
      市场概况
      - 时间区间:2025年9月16日至2026年1月23日(共90个交易日)
      - 开始价格:509.04
      - 结束价格:465.95
      - 总回报率(仅考虑股价变化):在该时间区间内为-8.46%(未包含股息)
      
      各项指标
      - 年化波动性:19.30%
      - 最大跌幅:-18.07%(该时间区间内的最大价格降幅)
      
      可能意味着什么
      - 这一时期市场出现了明显的下跌趋势,且波动性较高,导致在90个交易日后股价处于较低水平。
      - 大约18%的最大跌幅以及约19%的年化波动性说明,在这一时间段内,股价的波动幅度明显大于平均水平。
      - 如果你正在评估投资风险,那么这些数据说明了近期可能面临的风险。
      
      需要注意的事项
      - 这个总回报率仅反映了股价的变化情况,未包含股息因素。
      - 结果会因所选的时间区间不同而有所差异,未来的表现也可能出现显著变化。
      - 数据截至2026年1月23日,市场和基本面情况可能会迅速发生变化。如果你需要,我可以补充一些基本面的简要分析或相关新闻信息。
      

      回报率为-8.46%,这一数字说明了股价的下跌方向;最大跌幅为-18.07%,这个数据揭示了在这段时间内股价下降的最严重程度,而这一点通常才是投资者在谈论“风险”时真正关心的内容。年化波动性为19.30%,这一数值表明股价的波动幅度相当大。另外,请注意,所选的时间区间是明确指定的(2025年9月16日至2026年1月23日),正是这个原因使得这些指标具有可比性和重复性。

      演示3:仅包含新闻信息的“变化情况汇总”报告(除非有必要,否则不提供各项指标数据)

      <这就是“快速提供相关背景信息”的工作流程。输出内容应该保持叙述性,不得添加任何额外的指标数据,因为查询要求中明确禁止这样做。

      
      resp = run_agent(
          "Ticker: AAPL.US. 获取最新的7条头条新闻。"
          "用6到8行简短总结‘发生了哪些变化’,重点关注主题而非每条新闻的内容。"
          "除非有必要,否则不要计算收益数据。"
      )
      

      输出结果:

      
      ================================================================================
      查询内容:
      Ticker: AAPL.US. 获取最新的7条头条新闻,并用6到8行简短总结‘发生了哪些变化’,重点关注主题而非每条新闻的内容。除非有必要,否则不要计算收益数据。
      
      回复结果:
      ## AAPL.US – 7条最新头条新闻:按主题分类的简要总结
      
      变化内容(主题方面):
      - 宏观风险再度成为关注焦点:在美联储会议之前,期货价格出现下跌趋势,政府关门的相关担忧也影响了市场情绪。
      - 政策风险依然存在:关于关税和政府关门的新闻不断加剧政策层面的不确定性。
      - 关于资产配置的讨论仍在继续:关于小盘股与大盘股的投资策略的争论引发了关于资产多元化配置的讨论。
      - 对成长股与价值股的看法也存在分歧:SPTM与VTV、VOOG与IWO等投资组合的选择反映了不同的投资理念。
      - 盈利季报告为市场带来了额外的波动性,这种宏观层面的不确定性也与此有关。
      - 人工智能相关的话题越来越受到关注:一篇关于某位创始人的人工智能相关经历的报道表明,人们对利用人工智能进行投资的兴趣正在逐渐增加。
      

      这种处理方式确实做到了有效的信息压缩。它并没有简单地列出七条头条新闻就结束了整理工作,而是将它们按照不同的主题进行了分类(宏观风险、政策因素、资产配置策略、投资风格的变化、盈利季报告、人工智能相关话题)。更重要的是,它严格遵守了最初设定的要求——没有仅仅因为可以这样做就添加任何收益或风险相关的指标,而这正是如果这个工具只是作为产品内部的一个快速信息展示工具时所应该遵循的原则。

      构建Streamlit的最小可行产品

      在这个阶段,我们的目标并不是打造一个完美的用户界面,而是要开发出一个可以拿给团队成员查看的、能够正常使用的产品原型。

      如果你是唯一的使用者,使用笔记本来开发这个工具也是完全可行的。但一旦你需要从项目经理、创始人或其他非技术背景的人那里获得反馈,你就需要一个他们可以直接操作的工具。Streamlit正是将你的辅助功能整合成这种可用形式的最快方法,而且你完全不需要去构建复杂的前端开发环境。

      用户界面设计:以查询内容为核心,提供可选参数

      用户界面设计中最大的变化就是将查询内容作为主要的输入方式。因为人们实际上就是这样思考问题的——他们不会先列出“60个交易日的数据 + 基本面分析 + 头条新闻”这些信息,而是会首先提出一个具体的问题。

      因此,侧边栏应该设置一个查询内容输入框,用户可以在其中输入类似这样的请求:“给我提供关于AAPL的60天简要分析报告,包括基本面数据和5条头条新闻。”

      其他控制选项则可以作为可选参数来使用。这些并不是必须输入的内容,而是用于调整功能设置的选项。如果你的团队希望所有的分析报告中都必须包含基本面数据,你可以强制要求这样做;如果你采用以风险分析为核心的工作流程,也可以选择始终显示与风险相关的信息;如果对于你的具体使用需求来说,头条新闻带来的干扰太大,你也可以选择不显示它们。

      双窗格布局:左侧显示分析报告,右侧显示数据数值

      <一旦你点击“生成”按钮,你就应该确保生成的界面看起来更像一个产品展示页面,而不是一个聊天窗口。>

      1*ERqFXO59ZgbP5jMzRGgOfg

      左侧是市场简报。你可以将其复制到Slack中,或者放入每周的备忘录中。这种简报是以叙述性的形式呈现的,并且内容经过压缩。

      右侧则是基于各种工具生成的数据和分析结果。这些数据才是建立信任的基础。你可以直接查看回报区间、关键基本数据、风险指标以及头条新闻列表,而无需逐段阅读文字。这样也能清楚地了解模型实际上是从哪些工具中获取了这些信息的,而不是它自己如何解读这些数据的。

      i. 应用程序框架

      我们在这里并没有编写具体的逻辑代码,只是定义了应用程序的外壳结构,使得这个应用看起来更像是一个独立的产品界面,而不仅仅是一个笔记本中的单元格。

      import streamlit as st
      import pandas as pd
      from copilot import run_query
      
      st.set_page_config(page_title="Market Brief Copilot", layout="wide")
      
      st.title("Market Brief Copilot")
      st.caption("LangChain + EODHD. 最简化的内部风格简报,包含基于工具的数据分析。")
      

      这里最重要的代码行是from copilot import run_query。这一设计使得应用程序的结构更加清晰:Streamlit只负责提供用户界面,而copilot的具体逻辑则保存在copilot.py文件中。这样的分离结构使得将来如果你决定将同样的后端功能封装到FastAPI或其他内部用户界面系统中,也能轻松地重复使用这些代码。

      st.set_page_config(..., layout="wide")这一设置主要是出于用户体验的考虑。由于我们希望在左侧显示市场简报,在右侧展示基于工具的分析数据,因此采用宽布局可以让输出内容看起来更加整齐、不拥挤。

      ii. 输入面板

      这是用户界面中最重要的部分,因为它决定了人们如何使用这个工具。

      之所以采用“先输入查询条件再生成结果”的设计方式,是因为这与人们实际获取市场信息的方式是一致的。人们并不会先考虑应该选择哪些选项,而是会直接提出自己的问题。股票代码和时间区间这些选项仍然存在,但它们只是作为默认值提供的;只有当用户没有明确指定这些参数时,它们才会被自动使用。

      我们还添加了“可选参数”这一设置,主要是为了满足那些希望确保数据一致性需求的团队。例如,你可能希望每份简报中都包含基本的数据分析结果,即使用户在查询时没有特别要求。风险指标和头条新闻也是如此。

      with st.sidebar:
          st.header("输入参数")
      
          query = st.text_area("查询条件", value="针对AAPL.US股票,计算过去60个交易日的总回报率;获取市盈率和市净率数据;提取最新的5条头条新闻,并提供简短的分析说明。")
          default_ticker = st.text_input("默认股票代码(仅在用户未指定时使用)", value="AAPL.US")
          default_n_days = st.slider("默认的交易天数区间(仅在用户未指定时使用)", min_value=20, max_value=180, value=60, step=5)
      
          st.divider()
          
          with st.sidebar.expander("可选参数(强制包含)"):
              include_fund = stcheckbox("基本数据分析(市盈率、市净率等),是否包含", value=False)
              include_risk = stCheckbox("风险指标(波动性、回撤率等),是否包含", value=False)
              include_news = st.checkbox("头条新闻,是否包含", value=False)
              news_limit = st.slider("头条新闻的数量,最小值3,最大值10,默认值5,步长1,仅当选择包含头条新闻时才可见”, min_value=3, max_value=10, value=5, step=1, disabled=not include_news)
      
          run_btn = st.button("生成简报", type="primary")
      

      query文本区域是主要的输入界面。在演示中,你可以直接粘贴那些在测试代理时使用过的提示信息。这是有意为之的,这样就能确保产品界面与这款工具实际适用的工作流程保持一致。

      default_tickerdefault_n_days则是次要设置。只有当查询内容不够明确时,这些参数才会发挥作用。在产品环境中,它们的重要性远超表面上看起来的程度。人们经常会输入“给我一份60天的分析报告”,却忘记指定相关代码,因为他们认为上下文信息已经明确了。默认值的设置可以避免整个流程因此而失败。

      扩展功能的设计体现了“团队统一执行规则”的理念。默认情况下,这个功能是处于隐藏状态的,这样就不会干扰普通用户的界面使用体验。但当你需要使用固定的模板时,比如“每份报告都必须包含基本数据和标题”,这些控制选项就会显现出来。

      iii. 数据指标的展示

      分析报告固然有用,但在产品环境中,人们还需要能够方便地查看和重复使用这些数据数值。

      因此,我们将输出结果分为两个层次来处理:

      1. 文字描述(即Markdown格式的报告内容)。

      2. 结构化数据(如价格区间、基本数据、风险分析结果、标题等)。

      关键在于,我们不希望Streamlit仅仅为了展示数据指标就再次调用相关的工具。代理程序已经一次性调用了这些工具,因此我们可以直接从代理发送的消息中提取这些数据结果,并将其呈现在用户界面上。

      copilot.py中提取工具输出结果

      这个辅助函数会遍历LangGraph发送的所有消息,从中筛选出来自我们开发工具的数据信息。最终它会生成一个结构统一的artifacts字典,方便用户界面进行展示。

      def _extract_artifacts(messages: List[Any]) -> Dict[str, Any]:
          out = Dict[str, Any] = {}
          for m in messages:
              name = getattr(m, "name", None)
              content = getattr(m, "content", None)
      
              if not name:
                  continue
      
              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
      

      这就是连接“代理程序运行环境”与“用户界面”的桥梁。run_query()函数在执行结束后会同时返回brief_mdartifacts这两个结果。

      app.py中展示数据结果

      在Streamlit的开发框架中,我们将所有数据展示逻辑集中放在一个地方。_render_metrics()函数会接收artifacts字典,并将其转换成用户界面中可以显示的格式。

      def _render_metrics(artifacts: dict):
          cols = st.columns(3)
      
          price = artifacts.get("price")
          valuation = artifacts.get("valuation")
          risk = artifacts.get("risk")
          headlines = artifacts.get("headlines")
      
          with cols[0]:
              st.subheader("价格信息")
              if isinstance(price, dict) and "error" not in price:
                  st(metric("总回报", f"{price.get('total_return', 0.0) * 100:.2f}%")
                  stcaption(f"{price.get('start_date')} 至 {price.get('end_date')} . N={price.get('n')}")
                  st.write(
                      pd.DataFrame([price]).rename(
                          columns={
                              "first_close": "首次收盘价",
                              "last_close": "最终收盘价",
                              "total_return": "总回报",
                          }
                      ).T
                  )
              elif isinstance(price, dict) and "error" in price:
                  st.warning(price["error"])
              else:
                  st.info("没有价格相关数据输出(未请求该数据或相关工具未被使用)。")
      
          with cols[1]:
              st.subheader("基本信息")
              if isinstance(valuation, dict) and "error" not in valuation:
                  df = pd.DataFrame([valuation])
                  keep = ["ticker", "name", "sector", "market_cap", "pe", "pb", "beta", "dividend_yield", "profit_margin"]
                  keep = [c for c in keep if c in df.columns]
                  st.write(df[keep].T)
              elif isinstance(valuation, dict) and "error" in valuation:
                  st.warning(valuation["error"])
              else:
                  st.info("没有基本信息相关数据输出(未请求该数据或相关工具未被使用)。")
      
          with cols[2]:
              st.subheader("风险信息")
              if isinstance(risk, dict) and "error" not in risk:
                  st(metric("年波动率", f"{risk.get('volatility_ann', 0.0) * 100:.2f}%")
                  st.metric("最大回撤幅度", f"{risk.get('max_drawdown', 0.0) * 100:.2f}%")
                  stcaption(f"{risk.get('start_date')} 至 {risk.get('end_date')}. N={risk.get('n')}")
                  st.write(pd.DataFrame([risk]).T)
              elif isinstance(risk, dict) and "error" in risk:
                  st.warning(risk["error"])
              else:
                  st.info("没有风险相关数据输出(未请求该数据或相关工具未被使用)。")
      
          st.subheader("头条新闻")
          if isinstance(headlines, list) and len(headlines) > 0:
              for h in headlines:
                  title = h.get("title", "无标题")
                  link = h.get("link")
                  src = h.get("source")
                  dt = h.get("date")
                  line = f"- {title}"
                  if src:
                      line += f" ({src}")
                  if dt:
                      line += f" . {dt}
                  if link:
                      st.markdown(f"{line}  \n  {link}")
                  else:
                      st.markdown(line)
          else:
              st.info("没有头条新闻相关数据输出(未请求该数据或相关工具未被使用)。")
      

      这就是为什么整个应用程序会给人一种“像真正产品一样”的感觉。该模型能够生成简短的报告,但用户界面仍然会以固定的布局显示具体的数字数据。此外,我们并不会重新获取任何信息,而是直接展示在代理程序运行过程中已经返回的结果。

      iv. 将用户界面与引擎连接起来

      在这个阶段,Streamlit应用程序不应该进行复杂的处理,它只需要接收用户的输入,调用相应的函数,然后显示处理后的结果即可。

      最初,copilot.py中的函数是run_brief(ticker, n_days, …)。但当我们采用基于查询的用户界面设计后,这种函数结构就不再适用了。因此我们将后端函数修改为run_query(query, default_ticker, default_n_days, force_..., …)。这样,应用程序的结构依然简洁,但引擎的功能却变得足够灵活,能够处理真正符合产品风格的任务请求。

      这是更新后的run_query函数代码:

      def run_query(
          query: str,
          default_ticker: str = "AAPL.US",
          default_n_days: int = 60,
          force_fundamentals: bool = True,
          force_risk: bool = False,
          force_news: bool = True,
          news_limit: int = 5,
      ) -> Tuple[str, Dict[str, Any]]:
          
          q = (query or "").strip()
      
          if not q:
              q = f"For {default_ticker}, compute total return over the last {int(default_n_days)} trading days."
      
          constraints = [
              "约束条件:",
              "1) 必须使用现有的工具来获取数据,切勿自行编造数字。",
              "2> 不要直接输出原始的价格数据或冗长的新闻列表。",
              "3> 以清晰的Markdown格式输出结果,内容应包括:概览、各项指标、可能的意义以及需要注意的事项。",
              "4> 保持内容简洁且实用。",
              f"5> 如果查询中没有指定时间范围,那么默认使用过去{int(default_n_days)}个交易日的数据。",
              f"6> 如果查询中没有指定股票代码,那么默认使用{normalize_ticker(default_ticker)}作为查询对象。",
          ]
      
          if force_fundamentals:
              constraints.append("7> 必须包含基本财务指标(市盈率、市净率、市值、所属行业、贝塔系数)。请使用fundamentals_snapshot函数来获取这些数据。")
          if force_risk:
              constraints.append("8> 必须包含风险指标(年化波动率及最大回撤幅度)。请使用risk_metrics函数来获取这些数据。」
              constraints.append("9> 使用与计算回报范围相同的起始日期和结束日期来获取风险指标数据。")
          if force_news:
              constraints.append(f"10> 必须包含新闻标题。只提取前{int(news_limit)}条新闻,请使用latest_news函数来获取这些数据。")
      
          user_prompt = "用户查询:\n" + q + "\n\n" + "\n".join(constraints)
      
          response = AGENT.invoke(
              {"messages": [("system", system.prompt), ("user", userprompt)]}
          )
      
          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
      

      以下是app.py中的核心代码片段。这段代码仅在用户点击按钮时才会被执行。

      if run_btn:
          with stspinner("正在运行工具并生成报告……"):
              brief_md, artifacts = run_query(
                  query=query,
                  default_ticker=default_ticker,
                  default_n_days=default_n_days,
                  force_fundamentals=include_fund,
                  force_risk=include_risk,
                  force_news=include_news,
                  news_limit=news_limit,
              )
      
          left, right = st.columns([1.2, 1])
      
          with left:
              st.subheader("市场概览")
              st.markdown(brief_md)
      
          with right:
              st.subheader("基于工具的指标分析")
              _render_metrics(artifacts)
      
      else:
          st.info("请在左侧输入所需参数,然后点击**生成报告**。")
      

      该接口会返回两项内容,其工作原理与之前相同。brief_md代表左侧显示的Markdown简报;artifacts则是指那些可以在右侧直接呈现的工具输出结果,而无需再进行额外的API调用。

      重要的变化在于引擎现在的处理方式:原本由UI负责构建“request_parts”提示框,现在UI只需传递原始查询参数及相关执行标志即可。具体的执行逻辑被放在了run_query()函数内部,而非Streamlit框架中。这样的设计使得代码结构更加清晰,未来UI可能会发生变更,但产品的整体功能仍能保持一致。

      应用演示

      本部分展示了Streamlit最小可行产品版本的演示内容。这些示例查询语句可以直接在应用程序中使用,用于验证UI界面、工具调用结果以及简报内容的显示效果是否符合预期。

      演示1:基础信息简报(包含收益情况、估值数据及头条新闻)

      这是默认的“了解当前市场状况”的查询方式。该查询会要求系统结合价格变动趋势、基本面数据以及最新头条新闻来提供综合分析。

      查询语句:

      针对AAPL.US股票,计算过去60个交易日的总收益;获取市盈率和市净率数据;提取最近5条头条新闻,并对其进行简要解读。

      %[https://gumlet.tv/watch/6986e2cb4db88a967f4169a0/\]

      演示2:以风险分析为核心的工作流程(关注波动率与最大回撤幅度,不包含新闻内容)

      这种查询方式适用于评估投资风险或解释为何某个持仓会带来不适感,即使其整体收益并不极端。

      查询语句:

      针对MSFT.US股票,计算过去90个交易日的年化波动率及最大回撤幅度。报告内容应简洁明了,不包含头条新闻。

      %[https://gumlet.tv/watch/6986e4b54db88a967f4190e4/\]

      演示3:仅提供新闻信息的界面(突出重点主题,不显示其他指标数据)

      这种查询方式是最快捷的“了解市场变化”途径。其核心在于精简信息呈现,除非确实有必要,否则不会包含收益或风险相关的数据。

      查询语句:

      针对NVDA.US股票,提取最近7条头条新闻,并用6到8行文字总结市场变化情况。报告中只需提及相关主题,无需列出所有新闻标题;只有在确实需要时才计算收益数据。

      %[https://gumlet.tv/watch/6986e794924a60df4b1298c9/\]

      实用注意事项

      在实际使用中可能遇到的问题

      用户可能会输入各种不规范的股票代码格式,比如有人会输入aapl,也有人会输入AAPL,还有人会直接粘贴AAPL.US。如果不在前期对输入内容进行统一处理,就会花费大量时间去排查那些莫名其妙的API错误。因此才有了normalize_ticker()这个函数的存在。

      此外,还会遇到数据缺失的问题。有些股票代码对应的新闻信息可能不存在,某些基本面数据也可能为空值,有时价格查询接口也会返回空结果。这些工具本身就会生成相应的错误提示对象,Streamlit界面应该将这些错误以警告的形式显示出来,而不是让页面直接崩溃或显示空白内容。

      最大的“隐形杀手”其实是工具使用成本。eod_prices这个工具确实有用,但它是导致应用程序运行速度变慢、模型处理的数据量增加的最主要原因。因此,应该将其作为一种备用方案来使用;默认情况下,应选择那些占用资源较少的工具,比如60天摘要、基本数据快照以及头条列表等功能。

      最后,输出结果的一致性也是一个需要重视的问题。如果让系统自由运行,它就会开始执行额外的操作,从而导致输出格式逐渐变得混乱。解决这个问题的方法虽然有些繁琐,但却非常有效:必须严格控制输入提示的内容,保持工具集的简洁性,并确保输出格式的统一性。

      适合这种MVP架构的小型扩展功能

      一个简单的下一步就是实现多只股票代码的对比功能。使用相同的查询模式,针对两到三只股票进行查询,然后返回一份简洁的对比摘要。

      你还可以安排定期报告的功能。例如,可以每天或每周对关注的股票列表进行一次数据查询,然后将结果推送到Slack或通过电子邮件发送给相关人员。核心的处理流程依然是不变的。

      缓存也是另一个能够快速提升系统性能的方法。可以通过(股票代码, 时间窗口)这样的键值对来缓存工具的查询结果,这样在重复执行相同查询时,就不会频繁访问API,从而保证用户界面的响应速度保持迅速。

      如果你想将这个功能集成到正式的产品中,可以将run_query()函数封装在一个FastAPI接口后面。Streamlit仍然可以作为演示工具使用,而你的应用程序就可以像调用其他内部服务一样来使用这个后端接口。

      总结

      目前,你已经开发出了一个功能完备的市场辅助系统MVP版本。它能够接收自然语言形式的查询请求,通过相应的工具获取相关数据,然后返回一份简洁的摘要以及相关的指标信息供用户界面展示。真正的价值并不在于模型的响应结果,而在于这种可重复的工作流程,以及将数据处理引擎与应用程序清晰地分离开来这一设计。

      如果你正在开发金融科技产品,这种设计模式能够很好地满足用户的常见需求。团队通常已经掌握了各种基础数据(比如股票收盘价、基本财务信息、新闻报道等),但这些数据分散在不同的接口和仪表板上。加入一个辅助系统层后,就可以将这些数据整合成一份格式统一的市场报告,供项目经理、分析师或销售团队重复使用。此外,这种内部演示工具也非常实用,因为所有数据都清晰可见且易于追踪,而不会隐藏在聊天记录中。

      接下来最好的做法是,用真实的内部需求来测试这个系统一周时间,看看人们最常需要哪些功能。这样你就能判断是否需要添加缓存机制、多只股票代码的对比功能、定期报告功能,或者API封装层。目前这个MVP版本已经足够用来验证这些功能的需求了,无需过度开发。

Comments are closed.