智能体框架一:AutoGen

为何需要一个智能体框架?

  1. 提升代码复用与开发效率:无论是 ReAct 还是 Plan-and-Solve,都可以基于框架提供的标准组件快速搭建,从而避免重复劳动。
  2. 实现核心组件的解耦与可扩展性:一个健壮的智能体系统应该由多个松散耦合的模块组成。框架的设计会强制我们分离不同的关注点:
    • 模型层 (Model Layer):负责与大语言模型交互,可以轻松替换不同的模型(OpenAI, Anthropic, 本地模型)。
    • 工具层 (Tool Layer):提供标准化的工具定义、注册和执行接口,添加新工具不会影响其他代码。
    • 记忆层 (Memory Layer):处理短期和长期记忆,可以根据需求切换不同的记忆策略(如滑动窗口、摘要记忆)。 这种模块化的设计使得整个系统极具可扩展性,更换或升级任何一个组件都变得简单。
  3. 标准化复杂的状态管理:在真实的、长时运行的智能体应用中,状态管理是一个巨大的挑战,它需要处理上下文窗口限制、历史信息持久化、多轮对话状态跟踪等问题。一个框架可以提供一套强大而通用的状态管理机制,开发者无需每次都重新处理这些复杂问题。
  4. 简化可观测性与调试过程:当智能体的行为变得复杂时,理解其决策过程变得至关重要。一个精心设计的框架可以内置强大的可观测性能力。例如,通过引入事件回调机制(Callbacks),我们可以在智能体生命周期的关键节点(如 on_llm_starton_tool_endon_agent_finish)自动触发日志记录或数据上报,从而轻松地追踪和调试智能体的完整运行轨迹。

主流框架:

  • AutoGen:AutoGen 的核心思想是通过对话实现协作[1]。它将多智能体系统抽象为一个由多个“可对话”智能体组成的群聊。开发者可以定义不同角色(如 CoderProductManagerTester),并设定它们之间的交互规则(例如,Coder 写完代码后由 Tester 自动接管)。任务的解决过程,就是这些智能体在群聊中通过自动化消息传递,不断对话、协作、迭代直至最终目标达成的过程。
  • AgentScope:AgentScope 是一个专为多智能体应用设计的、功能全面的开发平台[2]。它的核心特点是易用性工程化。它提供了一套非常友好的编程接口,让开发者可以轻松定义智能体、构建通信网络,并管理整个应用的生命周期。其内置的消息传递机制和对分布式部署的支持,使其非常适合构建和运维复杂、大规模的多智能体系统。
  • CAMEL:CAMEL 提供了一种新颖的、名为角色扮演 (Role-Playing) 的协作方法[3]。其核心理念是,我们只需要为两个智能体(例如,AI研究员 和 Python程序员)设定好各自的角色和共同的任务目标,它们就能在“初始提示 (Inception Prompting)”的引导下,自主地进行多轮对话,相互启发、相互配合,共同完成任务。它极大地降低了设计多智能体对话流程的复杂度。
  • LangGraph:作为 LangChain 生态的扩展,LangGraph 另辟蹊径,将智能体的执行流程建模为图 (Graph)[4]。在传统的链式结构中,信息只能单向流动。而 LangGraph 将每一步操作(如调用LLM、执行工具)定义为图中的一个节点 (Node),并用边 (Edge) 来定义节点之间的跳转逻辑。这种设计天然支持循环 (Cycles),使得实现如 Reflection 这样的迭代、修正、自我反思的复杂工作流变得异常简单和直观。

AutoGen

核心机制:AutoGen 的设计哲学根植于”以对话驱动协作”。它巧妙地将复杂的任务解决流程,映射为不同角色的智能体之间的一系列自动化对话。基于这一核心理念,AutoGen 框架持续演进。

示例:实时显示比特币当前价格

软件开发的典型环节:从需求分析、技术选型、编码实现到代码审查和最终测试。

智能体团队角色:

  • ProductManager (产品经理): 负责将用户的模糊需求转化为清晰、可执行的开发计划。
  • Engineer (工程师): 依据开发计划,负责编写具体的应用程序代码。
  • CodeReviewer (代码审查员): 负责审查工程师提交的代码,确保其质量、可读性和健壮性。
  • UserProxy (用户代理): 代表最终用户,发起初始任务,并负责执行和验证最终交付的代码。

代码实现:

# 代码目录结构
autogen_demo:
/llm
  index.py
/roles
  pm.py
  enginer.py
  reviewer.py
  userProxy.py
/methods # 定义合作方式
  teamChat.py
/projects # 放置一些程序,解决用户问题

创建LLM客户端(/llm/index.py):

from autogen_ext.models.openai import OpenAIChatCompletionClient;
import os;
from dotenv import load_dotenv;

load_dotenv();

def create_model_client():
    return OpenAIChatCompletionClient(
        api_key=os.getenv('API_KEY'),
        model=os.getenv('MODEL_ID', 'deepseek-v4-flash'),
        base_url=os.getenv('BASE_URL'),
        model_info={ # deepseek非openai官方model,此字段必传
            'vision': True,
            'function_calling': True,
            'json_output': True,
            'family': 'unknown'
        }
    );

创建角色(roles/*)

产品经理(Product_Manager):

from autogen_agentchat.agents import AssistantAgent;

def create_product_manager(model_client):
    """创建产品经理智能体"""
    system_message = """你是一位经验丰富的产品经理,专门负责软件产品的需求分析和项目规划。

你的核心职责包括:
1. **需求分析**:深入理解用户需求,识别核心功能和边界条件
2. **技术规划**:基于需求制定清晰的技术实现路径
3. **风险评估**:识别潜在的技术风险和用户体验问题
4. **协调沟通**:与工程师和其他团队成员进行有效沟通

当接到开发任务时,请按以下结构进行分析:
1. 需求理解与分析
2. 功能模块划分
3. 技术选型建议
4. 实现优先级排序
5. 验收标准定义

请简洁明了地回应,并在分析完成后说"请工程师开始实现"。"""

    return AssistantAgent(
        name="ProductManager",
        model_client=model_client,
        system_message=system_message,
    )

工程师 (Engineer):

from autogen_agentchat.agents import AssistantAgent;

def create_engineer(model_client):
    """创建软件工程师智能体"""
    system_message = """你是一位资深的软件工程师,擅长 Python 开发和 Web 应用构建。

    你的技术专长包括:
    1. **Python 编程**:熟练掌握 Python 语法和最佳实践
    2. **Web 开发**:精通 Streamlit、Flask、Django 等框架
    3. **API 集成**:有丰富的第三方 API 集成经验
    4. **错误处理**:注重代码的健壮性和异常处理

    当收到开发任务时,请:
    1. 仔细分析技术需求
    2. 选择合适的技术方案
    3. 编写完整的代码实现
    4. 添加必要的注释和说明
    5. 考虑边界情况和异常处理

    请提供完整的可运行代码,并在完成后说"请代码审查员检查"。"""

    return AssistantAgent(
        name="Engineer",
        model_client=model_client,
        system_message=system_message,
    )

代码审查员(Code Reviewer):

from autogen_agentchat.agents import AssistantAgent;

def create_code_reviewer(model_client):
    """创建代码审查员智能体"""
    system_message = """你是一位经验丰富的代码审查专家,专注于代码质量和最佳实践。

    你的审查重点包括:
    1. **代码质量**:检查代码的可读性、可维护性和性能
    2. **安全性**:识别潜在的安全漏洞和风险点
    3. **最佳实践**:确保代码遵循行业标准和最佳实践
    4. **错误处理**:验证异常处理的完整性和合理性

    审查流程:
    1. 仔细阅读和理解代码逻辑
    2. 检查代码规范和最佳实践
    3. 识别潜在问题和改进点
    4. 提供具体的修改建议
    5. 评估代码的整体质量

    请提供具体的审查意见,完成后说"代码审查完成,请用户代理测试"。"""

    return AssistantAgent(
        name="CodeReviewer",
        model_client=model_client,
        system_message=system_message,
    )

用户代理(User Proxy):

from autogen_agentchat.agents import UserProxyAgent;

def create_user_proxy():
    """创建用户代理智能体"""
    return UserProxyAgent(
        name="UserProxy",
        description="""用户代理,负责以下职责:
    1. 代表用户提出开发需求
    2. 执行最终的代码实现
    3. 验证功能是否符合预期
    4. 提供用户反馈和建议

    完成测试后请回复 TERMINATE。""",
)

规定他们之间的合作流程(methods/teamchat)

from autogen_agentchat.teams import RoundRobinGroupChat;
from autogen_agentchat.conditions import TextMentionTermination;
from llm.index import create_model_client;
from roles.PM import create_product_manager;
from roles.Enginer import create_engineer;
from roles.Reviewer import create_code_reviewer;
from roles.UserProxy import create_user_proxy;

model_client = create_model_client();
product_manager = create_product_manager(model_client=model_client);
engineer = create_engineer(model_client=model_client);
code_reviewer = create_code_reviewer(model_client=model_client);
user_proxy = create_user_proxy();

# 定义团队聊天和协作规则
team_chat = RoundRobinGroupChat(
    participants=[
        product_manager,
        engineer,
        code_reviewer,
        user_proxy
    ],
    termination_condition=TextMentionTermination("TERMINATE"),
    max_turns=20,
)

接下来就可以合作一起做项目啦~

我们在/projects文件夹下,创建bitCoinInfo的项目文件:

from methods.teamChat import team_chat;
import asyncio;
from autogen_agentchat.ui import Console

async def run_software_development_team():
    # 定义任务描述
    task = """我们需要开发一个比特币价格显示应用,具体要求如下:
        核心功能:
        - 实时显示比特币当前价格(USD)
        - 显示24小时价格变化趋势(涨跌幅和涨跌额)
        - 提供价格刷新功能

        技术要求:
        - 使用 Streamlit 框架创建 Web 应用
        - 界面简洁美观,用户友好
        - 添加适当的错误处理和加载状态

        请团队协作完成这个任务,从需求分析到最终实现。"""
    
    # 异步执行团队协作,并流式输出对话过程
    result = await Console(team_chat.run_stream(task=task))
    return result

# 主程序入口
if __name__ == "__main__":
    result = asyncio.run(run_software_development_team())

直接运行刚刚的项目文件:python -m projects.bitCoinInfo

观察输出:

/Users/shane/dev/AutoGen/venv/lib/python3.14/site-packages/autogen_ext/models/openai/_openai_client.py:466: UserWarning: Missing required field 'structured_output' in ModelInfo. This field will be required in a future version of AutoGen.
  validate_model_info(self._model_info)
---------- TextMessage (user) ----------
我们需要开发一个比特币价格显示应用,具体要求如下:
        核心功能:
        - 实时显示比特币当前价格(USD)
        - 显示24小时价格变化趋势(涨跌幅和涨跌额)
        - 提供价格刷新功能

        技术要求:
        - 使用 Streamlit 框架创建 Web 应用
        - 界面简洁美观,用户友好
        - 添加适当的错误处理和加载状态

        请团队协作完成这个任务,从需求分析到最终实现。
---------- ThoughtEvent (ProductManager) ----------
我们被要求开发一个比特币价格显示应用,使用Streamlit框架。作为产品经理,我们需要进行需求分析、功能模块划分、技术选型、优先级排序、验收标准。然后请求工程师开始实现。

需求理解:
- 核心功能:实时显示BTC/USD价格,24小时变化趋势(涨跌幅和涨跌额),刷新功能。
- 技术要求:Streamlit,界面简洁美观,错误处理,加载状态。

需要分析API源:常见的免费API如CoinGecko、CoinCap、Binance等。建议使用CoinGecko的免费API。

功能模块:
1. 数据获取模块:从API获取BTC价格和24小时变化数据。
2. 显示模块:显示价格、涨跌幅、涨跌额,使用颜色区分涨跌。
3. 刷新模块:提供按钮或自动刷新(Streamlit的st.rerun或st.experimental_rerun)。
4. 错误处理:API请求失败时显示错误信息。
5. 加载状态:获取数据时显示spinner或进度条。

技术选型:Streamlit + requests库。

优先级排序:先实现核心数据获取和显示,再添加自动刷新和样式。

验收标准:
- 页面加载后自动显示比特币价格。
- 价格以绿色显示上涨,红色显示下跌。
- 显示24小时涨跌额和百分比。
- 有刷新按钮。
- 网络故障时显示友好错误。

现在输出分析结果,然后说"请工程师开始实现"。
---------- TextMessage (ProductManager) ----------
### 需求理解与分析
用户需要一个简洁、实时的比特币价格监控工具,核心关注点为当前价格、24小时价格变化趋势(涨跌幅与涨跌额)以及手动刷新能力。使用 Streamlit 框架意味着需要快速构建单页 Web 应用,强调低代码和交互性。技术选型上,应采用免费的加密货币 API(如 CoinGecko 公共 API)获取数据,无需用户登录。

### 功能模块划分
1. **数据获取模块**  
   - 调用 CoinGecko API `/simple/price` 获取 BTC/USD 当前价格及 24h 涨跌幅  
   - 处理超时、网络异常、API 限流等错误  
2. **价格显示模块**  
   - 大字体显示当前价格(USD)  
   - 涨跌额与涨跌幅并排显示,上涨用绿色(+),下跌用红色(-)  
   - 显示最后更新时间(或数据时效性提示)  
3. **交互控制模块**  
   - “刷新价格”按钮,触发 `st.rerun` 重新获取数据  
   - 数据加载时显示加载动画(`st.spinner`)  
   - 错误时显示提示信息而非崩溃  
4. **自动刷新(可选,低优先级)**  
   - 使用 `st.empty` 占位 + `time.sleep` 循环或 `streamlit-autorefresh` 组件  

### 技术选型建议
- **框架**:Streamlit(v1.28+)  
- **HTTP 库**:`requests`(Python 内置)  
- **数据源**:CoinGecko 免费 API(`https://api.coingecko.com/api/v3/simple/price?ids=bitcoin&vs_currencies=usd&include_24hr_change=true`)  
- **样式优化**:Streamlit 原生 markdown + CSS 自定义(通过 `st.markdown` 嵌入 `<style>`)  

### 实现优先级排序
1. **P0**:核心数据获取与价格/涨跌显示(含错误处理)  
2. **P1**:刷新按钮与加载状态  
3. **P2**:界面美化(颜色、对齐、字体)  
4. **P3**:自动刷新选项(视需求可推迟)  

### 验收标准
- 应用启动后自动展示比特币当前 USD 价格(保留两位小数)  
- 24h 涨跌额(如 +123.45)和涨跌幅(如 +2.34%)同时显示  
- 上涨时数字为绿色,下跌为红色,平盘为灰色  
- 点击“刷新”按钮后重新请求数据,期间有加载动画  
- 当 API 请求失败(网络断开、限流等),页面显示“数据获取失败,请稍后重试”而不是空白或异常  
- 页面整体布局整洁,标题清晰  

请工程师开始实现。
---------- ThoughtEvent (Engineer) ----------
我们被要求开发一个比特币价格显示应用,使用Streamlit。需要实现实时显示比特币当前价格(USD),24小时变化趋势(涨跌幅和涨跌额),刷新功能。需要添加错误处理和加载状态。

作为软件工程师,我们需要编写一个完整的Streamlit应用。我将在代码中包含:

- 导入必要的库:streamlit, requests, time(用于显示更新时间),datetime(可选)。
- 定义一个函数fetch_btc_price()来调用CoinGecko API:https://api.coingecko.com/api/v3/simple/price?ids=bitcoin&vs_currencies=usd&include_24hr_change=true
- 解析返回的JSON,提取usd价格和usd_24h_change(百分比变化)。还要计算涨跌额?API返回usd_24h_change是百分比变化,但我们需要涨跌额吗?24小时价格变化趋势通常包括涨跌幅(百分比)和涨跌额(具体金额)。CoinGecko的简单价格端点只提供24小时百分比变化。要获得24小时涨跌额,可能需要另一个API端点,比如/coins/bitcoin?localization=false&tickers=false&community_data=false&developer_data=false&sparkline=false,它包含market_data.price_change_24h_in_currency.usd。为了简单起见,我可能会选择只显示百分比变化,但需求明确要求“显示24小时价格变化趋势(涨跌幅和涨跌额)”。所以最好两者都提供。

我们可以使用两个请求:一个获取当前价格和24小时变化百分比,另一个获取24小时价格变化金额。或者使用/coins/bitcoin端点,一次获取所有信息。这个端点返回较慢但数据丰富。考虑到免费API限制,建议使用/coins/bitcoin的简洁请求。例如:https://api.coingecko.com/api/v3/coins/bitcoin?localization=false&tickers=false&community_data=false&developer_data=false&sparkline=false。响应中的market_data字段包含:current_price.usd, price_change_percentage_24h, price_change_24h_in_currency.usd。

我们可以使用这个端点来减少请求次数。代码中我们定义fetch_btc_data()返回字典包含价格、涨跌额、涨跌幅。

实现:

- 使用st.set_page_config设置标题和布局。
- 在页面顶部显示标题和子标题。
- 使用st.empty()作为占位符,用于后续更新数据。
- 定义一个按钮“刷新价格”,点击后调用st.rerun()触发重新运行。
- 在数据加载时使用st.spinner显示加载状态。
- 使用session_state或缓存来避免重复请求?无需缓存,因为每次重新运行都会请求新数据。
- 显示价格时,使用st.metric组件可以很好地显示指标和增量。st.metric(label, value, delta)其中delta可以是数字或字符串,自动显示颜色(绿色为正,红色为负)。这对我们很合适。我们可以用st.metric显示24小时变化,其中delta为涨跌额或涨跌幅?但需求要求显示涨跌幅和涨跌额两个数值。我们可以用两个st.metric并排?或者在一个st.metric中只显示一个,另一个通过markdown显示。为了简单,我们将涨跌幅和涨跌额合并为delta字符串,例如“+123.45 (2.34%)”。这样st.metric的delta会显示为字符串,但不会自动着色。我们可以手动用markdown着色。

更好的方法:使用自定义CSS和列布局。例如:第一列显示当前价格(大字体),第二列显示涨跌额(带颜色),第三列显示涨跌幅(带颜色)。这样更清晰。

考虑设计:让页面居中,使用大字体显示价格,并排显示变化。

我将采用以下布局:
- 使用st.columns(3)创建三个列:左列显示BTC图标或标签,中间列显示价格(大号),右列显示变化。
- 或者更简单的:顶部标题,然后一个大的metric显示当前价格,下面一行用两列分别显示涨跌额和涨跌幅。

我倾向于使用st.metric显示价格,然后在其下方用两列显示变化。但st.metric本身只支持一个delta,而我们需要两个delta。所以我们可以用两个st.metric并排,一个显示涨跌额,一个显示涨跌幅。但这样会显示两个指标和它们的标题,可能比较拥挤。我们可以用st.columns创建两个列,每个列用一个st.markdown自定义格式。

计划:
- 使用st.metric显示当前价格,label="BTC/USD",value=curent_price,delta=price_change_24h_in_usd(涨跌额),这样delta自动显示颜色,但我们需要同时显示涨跌幅。我们可以把涨跌幅放在子标题或旁边的文字中。例如在metric的帮助下,我们用st.write或st.caption显示涨跌幅。

但是需求说“显示24小时价格变化趋势(涨跌幅和涨跌额)”,并没有严格要求必须分开。我们可以将涨跌幅作为辅助信息显示。

为了更清晰,我决定:
- 使用st.metric显示当前价格,label="Bitcoin (BTC)", value="$ {price:,.2f}",delta=f"{change_24h_usd:+.2f} ({change_24h_pct:+.2f}%)",这样delta字符串包含两者。但st.metric的delta如果是字符串,不会自动着色,但我们可以通过CSS或HTML着色。实际上st.metric的delta参数如果传入字符串,Streamlit会原样显示,不会自动应用颜色。所以我们最好自定义。

自定义CSS:我们可以用markdown写一个包含样式的大块,显示价格和变化。但保持Streamlit的交互特性,我们仍然使用st.metric但将颜色手动控制。或者使用st.markdown配合HTML和CSS完全自定义。

我选择使用st.metric,但将涨跌额和涨跌幅合并为一个delta字符串,并借助CSS在字符串中嵌入颜色。但是st.metric的value和delta参数是纯文本,不能内嵌HTML。所以无法着色。

另一种方法:完全使用st.markdown构建布局,用大字体显示价格,用带颜色的文字显示变化。

我倾向于用st.markdown,因为更灵活。但需要保持好响应式。

具体实现:

1. 在app中定义一个函数get_btc_data(),使用requests.get调用CoinGecko API,设置超时10秒。
2. 解析响应,提取所需数据。
3. 在主函数中,使用streamlit的session_state记录错误状态。
4. 使用st.spinner在获取数据时显示。
5. 成功后将数据放入变量,显示在页面上。
6. 错误时显示错误信息。
7. 按钮的放置:放在st.button中,点击后调用st.rerun()。

注意:st.rerun()需要Streamlit 1.27.0+,我们假设用户有较新版本。

另外,添加自动刷新?可选,但需求没强制,不实现。

下面是代码的骨架:

```python
import streamlit as st
import requests
from datetime import datetime

st.set_page_config(page_title="Bitcoin Price Tracker", page_icon="₿", layout="centered")

def fetch_btc_data():
    url = "https://api.coingecko.com/api/v3/coins/bitcoin?localization=false&tickers=false&community_data=false&developer_data=false&sparkline=false"
    try:
        response = requests.get(url, timeout=10)
        response.raise_for_status()
        data = response.json()
        market_data = data['market_data']
        price = market_data['current_price']['usd']
        change_24h_usd = market_data['price_change_24h_in_currency']['usd']
        change_24h_pct = market_data['price_change_percentage_24h']
        return {
            'price': price,
            'change_usd': change_24h_usd,
            'change_pct': change_24h_pct,
            'last_updated': datetime.now().strftime("%Y-%m-%d %H:%M:%S")
        }
    except requests.exceptions.RequestException as e:
        st.error(f"Failed to fetch data: {e}")
        return None
    except KeyError as e:
        st.error(f"Unexpected API response structure: {e}")
        return None

def main():
    st.title("₿ Bitcoin Price Tracker")
    st.markdown("Real-time price and 24h change from CoinGecko")
    
    # Placeholder for data
    data_placeholder = st.empty()
    
    # Refresh button
    if st.button("🔄 Refresh Price"):
        st.rerun()
    
    # Fetch data
    with st.spinner("Fetching latest Bitcoin price..."):
        btc_data = fetch_btc_data()
    
    if btc_data:
        price = btc_data['price']
        change_usd = btc_data['change_usd']
        change_pct = btc_data['change_pct']
        last_updated = btc_data['last_updated']
        
        # Determine color class
        color_class = "green" if change_usd >= 0 else "red"
        
        # Display price using columns
        col1, col2, col3 = st.columns([2, 1, 1])
        with col1:
            st.markdown(f"<h1 style='font-size:3em;'>${price:,.2f}</h1>", unsafe_allow_html=True)
        with col2:
            st.markdown(f"<p style='font-size:1.2em; color:{color_class};'>{'+' if change_usd >= 0 else ''}${change_usd:,.2f}</p>", unsafe_allow_html=True)
        with col3:
            st.markdown(f"<p style='font-size:1.2em; color:{color_class};'>{'+' if change_pct >= 0 else ''}{change_pct:.2f}%</p>", unsafe_allow_html=True)
        
        st.caption(f"Last updated: {last_updated}")
    else:
        st.warning("Unable to fetch data. Please try again later.")
```

以上代码基本满足需求。但还有改进之处:页面顶部刷新按钮可能不够显眼,我们可以在价格显示区域附近放置按钮。另外,错误处理可以使用session_state来避免连续请求失败时一直显示错误?当前错误显示在页面上,但下次刷新会清除。已经可以。

注意:使用st.spinner时,如果数据获取失败,spinner会消失并显示错误信息。但spinner只包裹fetch_btc_data(),如果失败返回None,我们仍然可以显示warnings。很好。

另外,我们可以添加一个自动刷新功能(例如每60秒)使用streamlit_autorefresh,但需要额外安装,不强制。

对于布局,我选择三列显示价格、涨跌额、涨跌幅。但看起来可能需要更美观的对齐。或许使用st.metric的简洁方式更好。让我重新考虑:使用st.metric显示价格,然后在下面一行用两个st.metric分别显示涨跌额和涨跌幅。但这样会占据更多空间,且重复label。我们可以将涨跌额和涨跌幅作为辅助信息放在同一行。

我倾向于使用st.metric显示价格,并在其下方用两列显示变化。每个变化也用st.metric来显示,但label分别为“24h Change (USD)”和“24h Change (%)”。这样自动着色。但st.metric的delta只显示一个值,这里是涨跌额和涨跌幅本身值,不需要delta。实际上,st.metric用于显示指标值,可以用value来显示涨跌额或涨跌幅。但value的颜色不会自动根据正负变化,除非我们手动设置color。在st.metric中无法直接设置颜色。所以我们还是使用自定义HTML。

我决定采用自定义HTML,但为了更简洁,可以使用st.write配合emoji和颜色字体。

我最终采用方案:使用两个st.metric: 第一个显示当前价格(不带delta),第二个显示涨跌额(用delta参数传递涨跌额,自动颜色)。第三个显示涨跌幅(用delta参数传递涨跌幅,自动颜色)。但st.metric的delta参数接受一个数值,会自动着色。那么我们可以将涨跌额和涨跌幅分别作为两个单独的metric的delta,而value设为“24h Change”。但这样显示的是“24h Change: +123.45”这样,可能不太直观。

更合理的:使用四个metric: 价格(value=price),涨跌额(value=change_usd, label="24h Change (USD)"),涨跌幅(value=change_pct, label="24h Change (%)")。但value不会自动着色。st.metric的delta只显示变化方向,如果我们把value设为零或什么?不好。

最佳做法:用st.columns嵌入三个自定义HTML卡片。我选择最初的自定义HTML方案。

修改代码,使用st.markdown和CSS内置样式,确保在不同屏幕上的响应式。

此外,加入时间戳显示。

还有一个要点:价格变化趋势有时需要显示“涨跌额”和“涨跌幅”,我们用了$和百分号。注意正负号。

代码已经基本实现。还需要添加异常处理细节:如果API返回错误,例如超时,我们捕获异常并显示错误信息。

测试:假设用户运行。

最后加一句“请代码审查员检查”。

让我们写出最终代码。
---------- TextMessage (Engineer) ----------
```python
"""
比特币价格显示应用 - Streamlit 版本
功能:
- 实时显示比特币当前美元价格
- 显示24小时价格变化趋势(涨跌额和涨跌幅)
- 提供手动刷新按钮
- 错误处理与加载状态
"""

import streamlit as st
import requests
from datetime import datetime

# ==================== 页面配置 ====================
st.set_page_config(
    page_title="比特币价格追踪器",
    page_icon="₿",
    layout="centered",
    initial_sidebar_state="collapsed"
)

# ==================== 数据获取模块 ====================
def fetch_btc_data():
    """
    调用 CoinGecko API 获取比特币最新市场数据
    返回字典包含:价格、24h涨跌额、24h涨跌幅、更新时间
    若失败则返回 None
    """
    url = (
        "https://api.coingecko.com/api/v3/coins/bitcoin"
        "?localization=false"
        "&tickers=false"
        "&community_data=false"
        "&developer_data=false"
        "&sparkline=false"
    )
    try:
        response = requests.get(url, timeout=10)
        response.raise_for_status()                     # 抛出 HTTP 错误
        data = response.json()

        market_data = data['market_data']
        price = market_data['current_price']['usd']
        change_24h_usd = market_data['price_change_24h_in_currency']['usd']
        change_24h_pct = market_data['price_change_percentage_24h']

        return {
            'price': price,
            'change_usd': change_24h_usd,
            'change_pct': change_24h_pct,
            'last_updated': datetime.now().strftime("%Y-%m-%d %H:%M:%S")
        }
    except requests.exceptions.RequestException as e:
        st.error(f"网络请求失败: {e}")
        return None
    except KeyError as e:
        st.error(f"API 响应结构异常: {e}")
        return None
    except Exception as e:
        st.error(f"未知错误: {e}")
        return None

# ==================== 主界面 ====================
def main():
    st.title("₿ Bitcoin 价格追踪器")
    st.markdown("数据来源: CoinGecko · 实时更新")

    # ---------- 刷新按钮 ----------
    col_refresh, _ = st.columns([1, 5])
    with col_refresh:
        if st.button("🔄 刷新价格", use_container_width=True):
            st.rerun()

    # ---------- 获取数据 ----------
    with st.spinner("正在获取最新比特币价格 ..."):
        btc_data = fetch_btc_data()

    # ---------- 数据显示 ----------
    if btc_data is None:
        st.warning("⚠️ 数据获取失败,请稍后重试" )
        return  # 结束渲染,保留错误信息

    price = btc_data['price']
    change_usd = btc_data['change_usd']
    change_pct = btc_data['change_pct']
    last_updated = btc_data['last_updated']

    # 根据涨跌决定颜色
    color = "#22c55e" if change_usd >= 0 else "#ef4444"   # 绿色 / 红色
    arrow = "▲" if change_usd >= 0 else "▼"

    # ---------- 布局:三列(价格 | 涨跌额 | 涨跌幅)----------
    col1, col2, col3 = st.columns([3, 1.2, 1.2], gap="medium")

    with col1:
        st.markdown(
            f"<div style='font-size: 3.2rem; font-weight: 700; "
            f"line-height: 1.2;'>${price:,.2f}</div>",
            unsafe_allow_html=True
        )

    with col2:
        st.markdown(
            f"<div style='font-size: 1rem; color: {color}; "
            f"border-left: 2px solid #e2e8f0; padding-left: 12px;'>"
            f"<span style='font-size: 0.75rem; color: #64748b;'>变化额</span><br>"
            f"<span style='font-size: 1.3rem; font-weight: 600;'>{arrow} "
            f"{'+' if change_usd >= 0 else ''}${change_usd:,.2f}</span>"
            f"</div>",
            unsafe_allow_html=True
        )

    with col3:
        st.markdown(
            f"<div style='font-size: 1rem; color: {color}; "
            f"border-left: 2px solid #e2e8f0; padding-left: 12px;'>"
            f"<span style='font-size: 0.75rem; color: #64748b;'>变化率</span><br>"
            f"<span style='font-size: 1.3rem; font-weight: 600;'>{arrow} "
            f"{'+' if change_pct >= 0 else ''}{change_pct:.2f}%</span>"
            f"</div>",
            unsafe_allow_html=True
        )

    # ---------- 更新时间 ----------
    st.caption(f"最后更新: {last_updated}")

# ==================== 入口 ====================
if __name__ == "__main__":
    main()
```

---

请代码审查员检查。
---------- ThoughtEvent (CodeReviewer) ----------
我们被要求作为代码审查员审查这个比特币价格显示应用的代码。需要关注代码质量、安全性、最佳实践、错误处理。给出具体的审查意见,最后说"代码审查完成,请用户代理测试"。

我将逐行审查代码,注意:

1. 整体结构良好,注释清晰。
2. 使用了streamlit,页面配置,数据获取模块,主界面模块。
3. 关注点:
    - API请求是否正确?CoinGecko API 的URL是?当前是`/coins/bitcoin`端点,返回大量数据,不如用`/simple/price`更轻量。但当前代码也可以工作,只是速度可能稍慢。不过这不是错误,但可以建议优化。
    - 错误处理:使用了请求异常、KeyError、通用异常,捕获并显示错误信息,但st.error会直接显示在页面上,如果数据为None,会显示warning并返回。但注意:fetch_btc_data内部用了st.error,这会导致错误信息在页面上显示,然后主函数检查btc_data为None时又显示提示。这可能会重复显示错误信息。更好的做法是fetch_btc_data内部不直接调用st.error,而是抛出异常或者返回错误信息,让主函数统一处理。当前设计是捕获异常后st.error并返回None。然后主函数又st.warning。这样用户会看到两个提示:“网络请求失败”和“数据获取失败,请稍后重试”,可能有点冗余。可以优化。
    - 安全性:使用requests,无直接风险。但API返回的数据信任吗?我们只是显示,没有执行。
    - 性能:每次点击刷新都会重新请求,没问题。
    - 样式:使用了unsafe_allow_html,需要确保内容是安全的,这里是固定的格式字符串,没有问题。
    - 数据更新时间:用`datetime.now()`,不是真正的API返回时间。可以显示API返回的`last_updated_at`字段(如果有)。但CoinGecko的coins/bitcoin端点返回`last_updated`字段,是ISO时间,建议使用API的时间更准确。当前代码简单用本地时间,也可以接受,但最好使用API返回的时间。
    - 变量命名:合理。
    - 是否应该添加自动刷新?用户要求添加手动刷新即可,P3可选。当前没有自动刷新,符合要求。
    - 数据获取模块中的URL构建,使用f-string没问题。
    - 注意:当response.json()返回的数据中`market_data`可能不存在,或者`current_price`等字段不存在,KeyError会捕获,但可能不能区分具体字段缺失。可以更精确地使用dict的get方法或检查。
    - 还应该考虑API限流,没有特殊处理,可以建议添加重试逻辑。
    - 代码复用性:目前良好。
    - Streamlit rerun可能导致无限循环?点击按钮触发rerun,没问题。
    - 布局:col1, col2, col3比例合理。

其他:
- 是否应该使用session来减少连接开销?不是必须。
- 是否添加缓存?st.cache_data可以缓存数据一段时间(例如30秒),但手动刷新就不需要了。但如果不添加自动刷新,每次点击按钮都会请求,没有缓存问题。但建议使用st.cache_data来避免重复请求如果用户多次点击相同页面?实际上每次rerun都会重新执行脚本,cache_data可以避免在短时间内重复请求。但用户要求手动刷新,可能需要最新数据,缓存会使得刷新不更新。所以不使用缓存合理。

改进点:
1. 错误处理优化:避免双重错误提示。
2. 使用API返回的时间戳而非本地时间。
3. 使用更轻量的simple price API。

我将撰写审查意见,包括上述改进建议,并指出代码质量和安全性良好。最后说"代码审查完成,请用户代理测试"。
Enter your response: ---------- TextMessage (CodeReviewer) ----------
## 代码审查报告

### 总体评价
代码结构清晰、注释完整,功能完整实现了需求中的核心功能(价格显示、涨跌额/率、刷新按钮、错误处理与加载状态)。使用 Streamlit 框架和 CoinGecko API 完成数据获取,整体实现符合最佳实践。但仍有几点可优化之处,以提升数据准确性、用户体验和代码稳健性。

### 审查要点

#### 1. ✅ 代码质量与可读性
- **良好**:函数功能单一(`fetch_btc_data` 负责数据获取,`main` 负责 UI),变量命名清晰,注释充分。
- **改进建议**:  
  - `fetch_btc_data` 内部使用了 `st.error`,这会导致错误信息直接输出到主界面。若发生错误,后续在 `main` 中又会显示 `st.warning`,用户会看到两条提示(如“网络请求失败: ...”、“⚠️ 数据获取失败,请稍后重试”),略显冗余。建议让 `fetch_btc_data` 只返回数据或 `None`,由主函数统一管理错误消息。例如:  
    ```python
    except Exception as e:
        st.session_state.error_msg = str(e)
        return None
    ```
    然后在 `main` 中根据 `btc_data` 是否为 `None` 显示统一提示,避免重复。

#### 2. ✅ 安全性
- **安全**:所有用户输入均为固定 UI 控件,无外部注入风险。使用了 `unsafe_allow_html=True`,但渲染的内容均为程序生成的静态字符串(固定 HTML 模板 + 数值变量),无 XSS 风险。

#### 3. ⚠️ 数据时效性
- **问题**:`last_updated` 使用 `datetime.now()` 表示更新时间,实际上用户看到的是本地请求完成的时间,而非 CoinGecko 服务器生成该数据的真实时间。当网络延迟或 API 缓存时,该时间可能不准确。
- **建议**:使用 CoinGecko API 返回的 `last_updated` 字段(ISO 8601 格式),转换为本地时间后显示。示例:  
  ```python
  from datetime import datetime, timezone
  api_time = data['last_updated']  # 如 "2025-04-09T12:34:56.789Z"
  localized_time = datetime.fromisoformat(api_time.replace('Z', '+00:00')).astimezone()
  updated_str = localized_time.strftime("%Y-%m-%d %H:%M:%S")
  ```

#### 4. ⚙️ API 选择与性能
- **当前选择**:使用 `/coins/bitcoin` 端点返回完整市场数据(含 tickers、community_data 等),虽然通过参数禁用了部分字段,但仍比 `/simple/price?ids=bitcoin&vs_currencies=usd&include_24hr_change=true` 更重,响应速度稍慢。
- **建议**:切换至 `/simple/price` 端点,仅获取价格和 24h 变化,可减少数据量、提升加载速度。调整后的 `fetch_btc_data` 示例:  
  ```python
  url = (
      "https://api.coingecko.com/api/v3/simple/price"
      "?ids=bitcoin&vs_currencies=usd&include_24hr_change=true"
  )
  response = requests.get(url, timeout=10)
  data = response.json()
  price = data['bitcoin']['usd']
  change_24h_pct = data['bitcoin']['usd_24h_change']   # 该端点只返回百分比变化
  # 注意:simple price 端点不直接提供 24h 涨跌额(usd 变化量),需额外计算或使用其他端点
  ```
  若需同时显示涨跌额,仍建议保留 `/coins/bitcoin` 端点,但可进一步精简参数(如只取 `market_data` 部分)。当前实现已可接受,仅作优化提示。

#### 5. 🔄 用户体验与自动刷新
- **手动刷新**:通过 `st.button` + `st.rerun()` 实现,功能正常。  
- **缺失**:需求中未强制要求自动刷新,但可以考虑添加可选的自动刷新间隔(如每30秒),通过 `st.empty` 占位 + `time.sleep` 循环或 `streamlit-autorefresh` 组件实现。若用户未来需要可轻松扩展。

#### 6. 🧹 错误处理的健壮性
- **当前**:捕获了 `requests.exceptions.RequestException`、`KeyError`、通用 `Exception`,覆盖了大部分异常情况。  
- **可改进**:  
  - 未处理 API 限流(HTTP 429)重试。建议使用 `time.sleep(1)` 后重试1-2次,或检查响应头 `Retry-After`。  
  - `KeyError` 捕获范围较大,若 `market_data` 缺失但 `current_price` 存在,仍会捕获并报错。更精细的做法是使用 `.get()` 和条件判断,例如:  
    ```python
    market_data = data.get('market_data')
    if not market_data:
        return None
    price = market_data.get('current_price', {}).get('usd')
    if price is None:
        return None
    ```

#### 7. 📐 UI 细节
- **样式**:使用 CSS 实现涨跌颜色和箭头,简洁美观。  
- **布局**:`st.columns` 比例 `[3,1.2,1.2]` 在窄屏上可能拥挤,建议增大间距或设置 `st.columns` 的 `gap="large"`。  
- **刷新按钮**:放置于单独列,`use_container_width=True` 使按钮填满列宽,体验良好。可微调列宽度比例,使按钮更明显(如 `[1, 1, 4]`)。

### 最终建议
- **优先级高**:修复重复错误提示、使用 API 真实更新时间。  
- **优先级中**:考虑切换至更轻量 API 端点或保留当前并增加 API 限流重试。  
- **优先级低**:可选添加自动刷新功能、微调 UI 布局。

### 审查结论
代码整体质量优秀,核心功能已正确实现,错误处理和 UI 均达到要求。按上述建议调整后可进一步提升专业性和用户体验。

代码审查完成,请用户代理测试。

用AI最终输出的代码,试着跑一下:

类似文章

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注