Python在金融中的应用 · 第四部分

第十三节:私有化部署与校内 GPU API

这一节讨论模型不离开公有云的两种实现:在自己的 GPU 电脑上运行开源模型,或由教师将校内 GPU 服务器封装为受控 API。目标不是要求每个人购买显卡,而是理解模型权重、推理服务、权限和数据边界如何组成一条可维护的系统。

ModelScope 模型卡需要阅读的项目示意
模型卡教学示意:先检查模型身份、版本、硬件条件和许可证,再执行下载;不要只看模型名称。
请先选路线,再执行代码判断适合的路线本机 GPU 与 ModelScope教师端 FastAPI学生端调用安全与故障排查结业项目

不是每台电脑都应该运行本地大模型

本地运行 7B / 8B 级开源模型会消耗显存、磁盘和下载时间;普通笔记本不需要强行尝试。课程提供四种技术路线,它们的学习目标相同:行情和新闻先由 Python 整理,再由模型生成有边界的文字;差别在于“模型在哪里运行、数据如何流动”。

学生电脑、校内 FastAPI、GPU 推理与返回结果的架构示意
校内 GPU API 的结构示意。模型权重在服务器加载一次,学生端只请求受控接口,返回 JSON 格式的课程报告。
路线学生电脑需要什么数据流向最适合练习什么
公有云 API普通电脑与网络上下文发送到第三方服务请求、JSON、Prompt 和错误处理。
Coze Bot普通电脑与浏览器由平台处理 Bot 输入角色、工作流、测试和交互界面。
本机 GPU兼容的 NVIDIA GPU、足够显存与磁盘数据和模型都留在本机模型下载、加载和本地推理。
校内 GPU API普通电脑 + 校园网受控地发送到校内服务器客户端—服务端架构、权限和部署思维。
先做选择:没有 NVIDIA CUDA GPU 或者显存、磁盘不够时,直接跳到“校内 GPU API”部分。这不是能力不足,而是工程上的正常分工。

先做硬件检查,再下载任何模型

如果你计划走本机 GPU 路线,请打开 Trae 终端,在课程虚拟环境中运行下面命令。第一条检查系统是否能找到 NVIDIA 驱动;第二条由 PyTorch 检查 Python 是否能使用 CUDA。

nvidia-smi

python -c "import torch; print('CUDA 可用:', torch.cuda.is_available()); print('GPU:', torch.cuda.get_device_name(0) if torch.cuda.is_available() else '未找到')"
示例输出(有合适 GPU 的电脑) +-----------------------------------------------------------------------------+ | NVIDIA-SMI ... Driver Version: ... CUDA Version: ... | | GPU Name Memory-Usage | | NVIDIA GeForce RTX ... 820MiB / 16376MiB | +-----------------------------------------------------------------------------+ CUDA 可用: True GPU: NVIDIA GeForce RTX ...
nvidia-smi
NVIDIA 驱动提供的状态工具。它显示 GPU 名称、驱动版本、显存总量和已占用显存。如果终端显示“command not found”,该电脑通常不是 NVIDIA CUDA 环境。
torch.cuda.is_available()
检查当前 Python 环境中的 PyTorch 能否调用 CUDA。即使系统有显卡,若装了 CPU 版 PyTorch,它也可能返回 False。
get_device_name(0)
取得第 0 块 GPU 的名称。若没有 GPU,代码通过条件表达式打印“未找到”,避免直接报错。

Mac 电脑怎么办?Apple Silicon 使用的是不同的图形计算体系,不应照抄 CUDA / nvidia-smi 步骤。对于本课程的 7B/8B 本地 GPU 演示,推荐直接使用校内 GPU API 或云端 API 路线。

认识 ModelScope:先读模型卡,再下载

ModelScope(魔搭社区)是模型、数据集和相关工具的社区平台。模型页最重要的不是“下载”按钮,而是模型卡:它会说明模型 ID、基础模型、指令能力、许可证、下载大小、上下文长度、推荐硬件和运行示例。不同模型的加载方式可能不同,模型卡优先于任何课程示例。

  1. 登录并搜索模型

    打开 ModelScope 官网,搜索你有权使用的指令模型。课程示例以 Qwen 系列的 7B/8B 指令模型说明流程,不绑定某个永久版本。

  2. 阅读许可证

    查看许可、使用范围与是否需要同意协议。教学练习不等于自动取得商业用途、再分发或上传敏感数据的权限。

  3. 记录模型身份

    在 Notebook 顶部记录 model_id、revision(如有)、下载日期、Python 版本与显卡信息。模型更新后,同一 Prompt 的结果可能不同。

  4. 估计资源

    除模型文件外,还要为缓存、临时文件和推理显存留余量。显存不足时,先选择模型卡明确支持的量化或更小模型,不要随意删掉报错检查。

安装本机推理所需的包

python -m pip install -U modelscope transformers accelerate torch
python -c "import modelscope, transformers, torch; print('modelscope:', modelscope.__version__); print('torch:', torch.__version__)"
示例输出 modelscope: 1.xx.x torch: 2.xx.x

这里每个包的分工不同:modelscope 负责下载与管理模型资源;transformers 提供常见的 tokenizer 与模型加载接口;torch 执行张量和 GPU 计算;accelerate 帮助在可用设备间安排模型。安装成功不代表显存一定够用。

下载到明确的本地目录

第一次下载通常较慢。下面用 snapshot_download 把模型快照下载到本机缓存,并打印得到的目录。运行前将 model_id 改成模型卡上真实的 ID;不要手打一个不确定的名称。

from modelscope import snapshot_download

model_id = "Qwen/Qwen2.5-7B-Instruct"  # 以当前模型卡显示的 ID 为准
model_dir = snapshot_download(model_id=model_id)

print("模型下载或缓存目录:")
print(model_dir)
示例输出 模型下载或缓存目录: .../modelscope/hub/models/Qwen/Qwen2.5-7B-Instruct
snapshot_download
下载一个指定版本的完整模型快照;如果该版本已在本机缓存,通常直接返回目录,而不是重复下载。
model_id
模型的唯一标识。它不是显示名称;复制模型卡给出的 ID 能减少拼写错误。
model_dir
本地文件夹路径。之后 Transformers 从这个目录读取配置、tokenizer 与权重文件。

如果下载中断、磁盘满或权限不足,不要随意删除整个缓存目录。先记录报错、检查可用空间和网络,再按 ModelScope 文档处理缓存。

本机最小推理:先让模型回答一个无敏感信息的问题

首次加载模型时,先用一个与金融数据无关的小问题测试 tokenizer、模型和 GPU。不要一开始就把长行情表、新闻正文和真实账户信息送入模型;先确认基础环境能稳定输出。

import torch
from transformers import AutoTokenizer, AutoModelForCausalLM

if not torch.cuda.is_available():
    raise RuntimeError(
        "当前 Python 没有可用 CUDA GPU。请改用校内 GPU API 或云端路线。"
    )

tokenizer = AutoTokenizer.from_pretrained(model_dir)
model = AutoModelForCausalLM.from_pretrained(
    model_dir,
    torch_dtype="auto",
    device_map="auto",
)

messages = [
    {"role": "system", "content": "你是课程助理,回答必须简洁、客观。"},
    {"role": "user", "content": "为什么时间戳对事件驱动分析很重要?"},
]
text = tokenizer.apply_chat_template(
    messages, tokenize=False, add_generation_prompt=True
)
inputs = tokenizer([text], return_tensors="pt").to(model.device)

with torch.no_grad():
    generated = model.generate(**inputs, max_new_tokens=120, temperature=0.2)

new_tokens = generated[:, inputs.input_ids.shape[1]:]
answer = tokenizer.batch_decode(new_tokens, skip_special_tokens=True)[0]
print(answer)
示例输出 时间戳用于判断事件与价格变化的先后顺序,并帮助研究者核对信息是否已经在市场中公开。 没有统一时间,无法可靠区分事件发生前后的数据。
AutoTokenizer
加载“文字如何变成 token”的规则。不同模型的 tokenizer 可能不同,不能用 A 模型的 tokenizer 配 B 模型。
AutoModelForCausalLM
加载按顺序预测下一个 token 的语言模型。ForCausalLM 指自回归文本生成这一类任务。
torch_dtype="auto"
让加载器在可用条件下选择合适的浮点精度。显存与精度取舍必须以模型卡和硬件为准。
device_map="auto"
让加载器自动安排模型部件到可用设备。它不保证设备一定有足够空间;显存不足仍会报错。
apply_chat_template
按该模型要求把 system / user 消息转换成可推理文本。不要手写不属于该模型的特殊分隔符。
max_new_tokens
限制模型最多新生成多少 token,防止单次回答过长、太慢或占用太多显存。
torch.no_grad()
推理时不需要计算训练梯度;使用它可以减少不必要的内存开销。

再接入第十一节 context 的正确时机:只有上面的最小示例能稳定完成后,才把 user 内容替换为上一节的 context 与四段式输出要求。若最小示例都失败,先排查模型、GPU、驱动或依赖,而不是怀疑金融数据。

教师端:把 GPU 模型封装成校内 FastAPI 服务

对大多数学生而言,最佳折中是:教师在校内 GPU 服务器加载一次模型,提供一个受控接口;学生只发送经过第十一节处理的 context。FastAPI 的优势是:请求体有清晰结构,自动生成交互文档,可在 /docs 检查接口。以下代码是教学用最小服务,不是直接暴露到公网的生产方案。

服务器项目的文件结构

school-finance-agent/
├── server.py          # FastAPI 服务端
├── .env               # 模型 ID 与访问 token,只留在服务器
├── requirements.txt   # 依赖版本
└── logs/              # 只保存脱敏的运行日志(如课程需要)

服务器端 .env 示例

TEACHING_API_TOKEN=请生成一串足够长的随机 token
MODEL_ID=Qwen/Qwen2.5-7B-Instruct

server.py:从健康检查到受保护的 POST 接口

import os
import torch
from dotenv import load_dotenv
from fastapi import FastAPI, Header, HTTPException
from pydantic import BaseModel, Field
from transformers import AutoTokenizer, AutoModelForCausalLM

load_dotenv()
ACCESS_TOKEN = os.environ["TEACHING_API_TOKEN"]
MODEL_ID = os.environ["MODEL_ID"]

app = FastAPI(
    title="Teaching Finance Event Agent",
    description="仅限课程内网教学使用;不提供投资建议。",
)

class AgentRequest(BaseModel):
    prompt: str = Field(min_length=20, max_length=12000)

@app.get("/health")
def health():
    return {
        "status": "ok",
        "model": MODEL_ID,
        "cuda_available": torch.cuda.is_available(),
    }

tokenizer = AutoTokenizer.from_pretrained(MODEL_ID)
model = AutoModelForCausalLM.from_pretrained(
    MODEL_ID, torch_dtype="auto", device_map="auto"
)

def require_token(authorization: str | None):
    if authorization != f"Bearer {ACCESS_TOKEN}":
        raise HTTPException(status_code=401, detail="invalid token")

@app.post("/finance-agent")
def finance_agent(
    body: AgentRequest,
    authorization: str | None = Header(default=None),
):
    require_token(authorization)

    messages = [
        {
            "role": "system",
            "content": "你是课程金融信息整理助手。不得提供投资建议。"
        },
        {"role": "user", "content": body.prompt},
    ]
    text = tokenizer.apply_chat_template(
        messages, tokenize=False, add_generation_prompt=True
    )
    inputs = tokenizer([text], return_tensors="pt").to(model.device)

    with torch.no_grad():
        outputs = model.generate(
            **inputs, max_new_tokens=350, temperature=0.2
        )

    new_tokens = outputs[:, inputs.input_ids.shape[1]:]
    report = tokenizer.batch_decode(
        new_tokens, skip_special_tokens=True
    )[0].strip()

    return {"analysis_report": report, "model": MODEL_ID}
服务器代码部分具体作用为什么不能省略
load_dotenv()读取服务器本机的密钥和模型 ID。避免把 token 写在 server.py 中,也便于换模型。
AgentRequest规定请求体必须有 prompt,并限制最小、最大长度。错误输入能更早返回清楚错误,避免超长文本耗尽显存。
/health不做模型推理,只报告服务和 CUDA 状态。学生请求失败时,先判断是网络问题还是推理问题。
Header读取 HTTP Authorization 头。没有访问 token 的请求应被拒绝,避免任何人调用 GPU。
require_token将鉴权判断单独写成函数。便于检查,也避免以后新增接口时漏掉权限检查。
max_new_tokens限制每个请求的最大输出长度。防止一个请求占用过多显存和时间,影响其他同学。

安装与启动服务

python -m pip install -U "fastapi[standard]" python-dotenv transformers torch
fastapi dev server.py
示例输出 INFO: Uvicorn running on http://127.0.0.1:8000 INFO: Application startup complete. 打开 http://127.0.0.1:8000/docs 可见自动生成的接口文档。

“127.0.0.1”只代表服务器自己。它适合先在服务器本地测试。是否允许校园内其他电脑访问、应使用哪个内网地址、是否需要反向代理和 HTTPS,必须由教师和学校网络管理员按校内安全要求配置;不要为求方便将 8000 端口直接暴露到公网。

学生端:先检查服务健康,再发送 context

学生端不下载模型,也不需要 transformers。它只要有上一节的 context、校内服务器地址和由教师单独提供的访问 token。示例地址中的 IP 只是占位符,必须替换为教师公布的校内地址;不要把 token 写入作业截图。

学生端 .env 增加两行

TEACHING_AGENT_URL=http://教师公布的校内地址:8000
TEACHING_API_TOKEN=教师单独提供的访问 token

第一步:调用 health,不消耗模型推理

from dotenv import load_dotenv
import os
import requests

load_dotenv()
base_url = os.environ["TEACHING_AGENT_URL"].rstrip("/")

health_response = requests.get(f"{base_url}/health", timeout=10)
print("状态码:", health_response.status_code)
health_response.raise_for_status()
print(health_response.json())
示例输出 状态码: 200 {'status': 'ok', 'model': 'Qwen/…', 'cuda_available': True}

第二步:发送第十一节生成的 context

access_token = os.environ["TEACHING_API_TOKEN"]

headers = {
    "Authorization": f"Bearer {access_token}",
    "Content-Type": "application/json",
}
body = {"prompt": user_prompt}  # user_prompt 来自第十二节的四段式任务

response = requests.post(
    f"{base_url}/finance-agent",
    headers=headers,
    json=body,
    timeout=120,
)

print("状态码:", response.status_code)
response.raise_for_status()
result = response.json()

print("服务器使用的模型:", result["model"])
print("分析报告:")
print(result["analysis_report"])
示例输出 状态码: 200 服务器使用的模型: Qwen/… 分析报告: 一、可直接从数据读出的事实 - … 二、新闻可能涉及的影响渠道 - … 三、目前无法确认的内容 - … 四、数据范围与局限性 - …
rstrip("/")
删除服务器地址末尾可能多出的斜杠,避免拼接 /health 时得到双斜杠。
GET /health
轻量检查。若这里就失败,先查校园网、地址、服务器是否运行,不要直接重复推理请求。
Authorization
把 token 作为 Bearer 凭证发送。token 不等于用户名,也不能出现在 Git 或网页源码里。
json=body
请求体只包含一个 Prompt。不要把完整 CSV、Excel、身份证号、账户号或无关隐私数据发送到服务端。
timeout=120
本地模型生成可能比云端慢,因此给出更长等待;如果频繁超时,应由教师检查并发与服务器资源。

安全、权限与故障排查

校内服务的“私有化”是一个系统问题,不只是把模型换到服务器。下面的清单适用于教学环境;正式科研或对外服务还需要学校信息安全、数据管理和网络管理要求。

最小数据只发送需要的行情窗口与新闻摘要,不发送原始账户、个人身份或无关数据。
最小权限访问 token 只给课程成员;学生离课或 token 泄露后及时撤销或更换。
网络边界只允许校内可信网络访问;不要将调试端口直接开放到公网。
日志脱敏记录状态、耗时、错误类型即可;不要把完整 Prompt、token 或隐私数据写入日志。
学生看到的错误含义学生应做什么
连接超时 / ConnectionError网络到服务器不通,或服务未启动。确认已连接校园网;先访问 /health;将报错时间发给教师。
401 invalid token未携带、携带了错误 token,或 token 已被撤销。检查 .env 变量名,切勿把 token 发到群里。
422 validation error请求 JSON 不符合 AgentRequest 的结构或长度限制。检查是否使用 {"prompt": user_prompt},以及 Prompt 是否过长。
500服务器模型加载、显存或代码出现异常。不要反复重试;保存状态码、时间和简短错误信息交给教师。
返回格式不对服务升级或代码期望字段不一致。先 print(response.text[:500]),再对照教师公布的 API 文档。

绝对不要做:把教师的内网 token 放进公开 GitHub;将服务器地址、模型端口和调试密码发布到公开网页;为了绕过权限把服务改成无鉴权;或将课程报告当作真实交易决策工具。

结业项目:四选一,标准不降低

你可以选择公有云 API、Coze、本机 GPU 或校内 GPU API 之一。选择不同,不代表评分标准不同:每条路线都必须说明数据从哪里来、什么时候抓取、模型看到了什么、输出哪里不确定,以及为什么不能据此荐股。

  1. 提交可运行代码

    至少包含:数据读取、字段检查、预处理、Prompt 组装、模型 / API 调用、响应解析、文件保存和错误处理。代码要有注释,但注释不能替代实际检查。

  2. 提交可复现材料

    提交 requirements、模型或 API 版本、日期范围、脱敏样例输出、运行截图与目录说明。密钥、Cookie、账号和受限原始数据不得提交。

  3. 提交一份短报告

    建议 1,500 字以内,包含架构图、数据字典、测试案例、一个失败情况、输出边界和后续改进。报告要明确区分“程序自动生成的文字”与“你的人工判断”。

  4. 可选扩展

    多标的批处理、定时生成日报、指标计算、RAG 知识库或工具调用都可以加分;但每个扩展都必须增加数据来源、权限和失败处理说明。

本部分真正要学会的事:模型可以是云端、校内或本机;但一份可信的课程项目始终依赖同一条原则——输入可查、过程可复现、错误可定位、结论有边界。