4、LLM基础调用:Prompt模板设计、结构化输出(Pydantic)、流式输出

好,咱们直接进入正题。这一章聊的是LLM调用的基本功,说白了就是怎么跟大模型好好说话,并且让它按你的规矩办事。

我刚开始做量化投研Agent的时候,踩过不少坑。最典型的就是:明明问的是同一份财报,模型今天给个表格,明天给段散文,后天直接甩我一句“我不确定”。嗯,这其实就是Prompt设计没到位,输出格式没约束住。

所以这一章,我带你搞定三件事:Prompt模板设计结构化输出(Pydantic)流式输出。这三板斧砍下去,你的Agent才算真正有了“职业素养”。

4.1 Prompt模板设计:别让模型猜你的心思

你想想看,如果你跟一个实习生说“帮我看看这份财报”,他可能给你一堆废话。但如果你说“请提取营收、净利润、毛利率,并判断是否超预期”,他就能精准干活。

大模型也一样。Prompt模板就是你的“标准化指令”。

核心原则: 角色 + 任务 + 格式 + 示例

我个人习惯把模板拆成四个部分:

  • 角色设定:告诉模型你是谁。比如“你是一名资深量化分析师”。
  • 任务描述:具体要做什么。比如“解读以下财报文本”。
  • 输出格式:明确要什么结构。比如“返回JSON格式”。
  • 示例(Few-shot):给一两个例子,模型学得最快。

举个例子,我在项目中常用的模板长这样:

system_prompt = """
你是一名专业的财报解读助手。
请根据用户提供的财报文本,提取关键财务指标。
输出格式必须为JSON,包含以下字段:
- revenue: 营收(亿元)
- net_profit: 净利润(亿元)
- gross_margin: 毛利率(百分比)
- sentiment: 情绪判断(positive/negative/neutral)

示例:
输入:公司2023年营收100亿元,净利润20亿元,毛利率40%。
输出:{"revenue": 100, "net_profit": 20, "gross_margin": 40, "sentiment": "positive"}
"""

为什么要这么设计?说白了,就是减少模型的“自由发挥空间”。你给的结构越清晰,它越不容易跑偏。

小技巧: 我建议在模板里加一句“如果数据缺失,请返回null”。这样能避免模型瞎编数字。我曾经吃过这个亏,模型把“暂无数据”直接编成了“0”,差点导致策略信号出错。

4.2 结构化输出(Pydantic):让数据规规矩矩

Prompt模板虽然能约束格式,但返回的还是字符串。你想想看,如果模型返回的JSON里字段名拼错了,或者类型不对,你的后续代码直接崩掉。

这时候就需要Pydantic上场了。它是Python里做数据验证的利器,能帮你把模型的输出“格式化”成标准对象。

我一般这么用:

from pydantic import BaseModel, Field
from typing import Optional

class FinancialReport(BaseModel):
    revenue: float = Field(..., description="营收(亿元)")
    net_profit: float = Field(..., description="净利润(亿元)")
    gross_margin: Optional[float] = Field(None, description="毛利率(百分比)")
    sentiment: str = Field(..., pattern="^(positive|negative|neutral)$")

# 然后让LLM输出JSON,再用Pydantic解析
raw_output = '{"revenue": 100, "net_profit": 20, "gross_margin": 40, "sentiment": "positive"}'
report = FinancialReport.model_validate_json(raw_output)
print(report.revenue)  # 100.0

你看,这样一搞,数据就变得规规矩矩了。类型不对?Pydantic直接抛异常。字段缺失?它也能告诉你。

避坑指南: 我曾经遇到过模型返回的JSON里多了一个逗号,或者用了单引号。Pydantic默认是严格模式,会报错。我的建议是:先用json.loads()做一次宽松解析,再用Pydantic校验。或者直接用model_validate_json(),它内部会先做一次JSON解析。

另外,Pydantic还支持嵌套模型。比如你要解析一份完整的财报,里面包含多个子表:

class BalanceSheet(BaseModel):
    total_assets: float
    total_liabilities: float

class FullReport(BaseModel):
    income: FinancialReport
    balance: BalanceSheet

这样你的数据结构就非常清晰了。说白了,Pydantic就是给LLM的输出加了一层“安检门”。

4.3 流式输出:别让用户干等

你有没有遇到过这种情况:调用LLM分析一份长财报,结果等了十几秒才看到结果?用户体验极差。

流式输出(Streaming)就是解决这个问题的。它让模型一边生成一边返回,用户能看到“打字效果”,心理等待时间大大缩短。

我常用的实现方式有两种:

  • OpenAI SDK原生支持:设置stream=True
  • 自定义生成器:用yield逐块返回

代码示例:

from openai import OpenAI

client = OpenAI(api_key="your-key")

def stream_analysis(text: str):
    response = client.chat.completions.create(
        model="gpt-4",
        messages=[
            {"role": "system", "content": "你是一名财报分析师,请逐句解读。"},
            {"role": "user", "content": text}
        ],
        stream=True  # 开启流式
    )
    
    for chunk in response:
        if chunk.choices[0].delta.content:
            yield chunk.choices[0].delta.content

# 使用
for token in stream_analysis("公司2023年营收增长20%..."):
    print(token, end="", flush=True)

嗯,这里要注意一点:流式输出时,不要做后处理。比如你打算把输出转成JSON,那就别用流式,因为JSON是完整的结构,拆开就没意义了。

我的经验: 在投研Agent里,我通常把流式输出用在“解读”环节,比如让模型逐段分析财报。而“提取数据”这种需要结构化输出的任务,我选择非流式,一次性拿到完整JSON再用Pydantic校验。

4.4 知识体系总览

为了让你更直观地理解这三者的关系,我画了一张图:

LLM基础调用知识体系 Prompt模板设计 结构化输出(Pydantic) 流式输出 角色 + 任务 + 格式 + 示例 数据校验 + 类型约束 + 嵌套 逐块返回 + 实时展示 应用场景 财报解读:Prompt模板定义角色 → 流式输出逐句分析 数据提取:Prompt模板定义格式 → Pydantic校验 → 非流式输出 三者配合使用,才能构建稳定可靠的投研Agent

从这张图你能看出来,这三个模块不是孤立的。Prompt模板是“入口”,决定了模型怎么理解任务;结构化输出是“质检员”,保证数据不出错;流式输出是“体验优化”,让用户不焦虑。

我个人建议,在实际项目中,先把Prompt模板和Pydantic搭好,再考虑流式输出。因为数据准确性永远是第一位的,体验是锦上添花。

好了,这一章的内容就这些。记住:跟大模型打交道,规矩定得越细,后面越省心


专注资料整理