跳到内容

使用 Instructor + Burr 的抽认卡生成器

抽认卡有助于分解复杂主题,学习从生物学到新语言或戏剧台词的任何内容。这篇博客将展示如何使用 LLM 生成抽认卡,开启你的学习之旅!

Instructor 使我们能够可靠地从 LLM 获取结构化输出,而 Burr 有助于创建易于理解和调试的 LLM 应用。它附带 Burr UI,一个免费、开源、本地优先的工具,用于可观测性、标注等!

信息

本文是对之前一篇博文的扩展:使用 Instructor 分析 YouTube 字幕

使用 Instructor 通过 LLM 生成抽认卡

pip install openai instructor pydantic youtube_transcript_api "burr[start]"

1. 定义 LLM 响应模型

使用 instructor,你可以定义 Pydantic 模型,这些模型将作为 LLM 填充的模板。

在这里,我们定义了 QuestionAnswer 模型,它将存储问题、答案和一些元数据。没有默认值的属性将由 LLM 生成。

import uuid

from pydantic import BaseModel, Field
from pydantic.json_schema import SkipJsonSchema


class QuestionAnswer(BaseModel):
    question: str = Field(description="Question about the topic")
    options: list[str] = Field(
        description="Potential answers to the question.", min_items=3, max_items=5
    )
    answer_index: int = Field(
        description="Index of the correct answer options (starting from 0).", ge=0, lt=5
    )
    difficulty: int = Field(
        description="Difficulty of this question from 1 to 5, 5 being the most difficult.",
        gt=0,
        le=5,
    )
    youtube_url: SkipJsonSchema[str | None] = None
    id: uuid.UUID = Field(description="Unique identifier", default_factory=uuid.uuid4)

这个例子展示了 instructor 的几个特性

  • Field 可以有一个 defaultdefault_factory 值,以防止 LLM 虚构值
    • id 生成唯一的 id (uuid)
  • 类型注解 SkipJsonSchema 也阻止 LLM 生成该值。
    • youtube_url 在应用中通过编程设置。我们不希望 LLM 虚构它。
  • Field 可以对 LLM 生成的内容设置约束。
    • min_items=3, max_items=5 将潜在答案的数量限制在 3 到 5 之间
    • ge=0, lt=5 将难度限制在 0 到 5 之间,其中 5 是最难的

2. 获取 YouTube 字幕

我们使用 youtube-transcript-api 来获取视频的完整字幕。

from youtube_transcript_api import YouTubeTranscriptApi

youtube_url = "https://www.youtube.com/watch?v=hqutVJyd3TI"
_, _, video_id = youtube_url.partition("?v=")
segments = YouTubeTranscriptApi.get_transcript(video_id)
transcript = " ".join([s["text"] for s in segments])

3. 生成问答对

现在,生成问答对

  1. 通过包装 OpenAI 客户端创建一个 instructor 客户端
  2. instructor_client 上使用 .create_iterable() 从输入生成多个输出
  3. 指定 response_model=QuestionAnswer 以确保输出是 QuestionAnswer 对象
  4. 使用 messages 通过 system 消息传递任务指令,并通过 user 消息传递输入字幕。
import instructor
import openai

instructor_client = instructor.from_openai(openai.OpenAI())

system_prompt = """Analyze the given YouTube transcript and generate question-answer pairs
to help study and understand the topic better. Please rate all questions from 1 to 5
based on their difficulty."""

response = instructor_client.chat.completions.create_iterable(
    model="gpt-4o-mini",
    response_model=QuestionAnswer,
    messages=[
        {"role": "system", "content": system_prompt},
        {"role": "user", "content": transcript},
    ],
)

这将返回一个生成器,你可以迭代它来访问单个 QuestionAnswer 对象。

print("Preview:\n")
count = 0
for qna in response:
    if count > 2:
        break
    print(qna.question)
    print(qna.options)
    print()
    count += 1

"""
Preview:

What is the primary purpose of the new OpenTelemetry instrumentation released with Burr?
['To reduce code complexity', 'To provide full instrumentation without changing code', 'To couple the project with OpenAI', 'To enhance customer support']

What do you need to install to use the OpenTelemetry instrumentation with Burr applications?
['Only OpenAI package', 'Specific OpenTelemetry instrumentation module', 'All available packages', 'No installation needed']

What advantage does OpenTelemetry provide in the context of instrumentation?
['It is vendor agnostic', 'It requires complex integration', 'It relies on specific vendors', 'It makes applications slower']
"""

使用 Burr 创建抽认卡应用

Burr 使用 actionstransitions 来定义复杂应用,同时保持流程图的简洁性,便于理解和调试。

1. 定义 actions

Actions 是你的应用可以执行的操作。@action 装饰器指定可以从 State 读取或写入哪些值。装饰后的函数将 State 作为第一个参数,并返回一个更新后的 State 对象。

接下来,我们定义三个 actions

  • 处理用户输入以获取 YouTube URL
  • 获取与该 URL 相关的 YouTube 字幕
  • 为字幕生成问答对

请注意,这只是对之前代码片段的轻微重构。

from burr.core import action, State


@action(reads=[], writes=["youtube_url"])
def process_user_input(state: State, user_input: str) -> State:
    """Process user input and update the YouTube URL."""
    youtube_url = (
        user_input  # In practice, we would have more complex validation logic.
    )
    return state.update(youtube_url=youtube_url)


@action(reads=["youtube_url"], writes=["transcript"])
def get_youtube_transcript(state: State) -> State:
    """Get the official YouTube transcript for a video given it's URL"""
    youtube_url = state["youtube_url"]

    _, _, video_id = youtube_url.partition("?v=")
    transcript = YouTubeTranscriptApi.get_transcript(video_id)
    full_transcript = " ".join([entry["text"] for entry in transcript])

    # store the transcript in state
    return state.update(transcript=full_transcript, youtube_url=youtube_url)


@action(reads=["transcript", "youtube_url"], writes=["question_answers"])
def generate_question_and_answers(state: State) -> State:
    """Generate `QuestionAnswer` from a YouTube transcript using an LLM."""
    # read the transcript from state
    transcript = state["transcript"]
    youtube_url = state["youtube_url"]

    # create the instructor client
    instructor_client = instructor.from_openai(openai.OpenAI())
    system_prompt = (
        "Analyze the given YouTube transcript and generate question-answer pairs"
        " to help study and understand the topic better. Please rate all questions from 1 to 5"
        " based on their difficulty."
    )
    response = instructor_client.chat.completions.create_iterable(
        model="gpt-4o-mini",
        response_model=QuestionAnswer,
        messages=[
            {"role": "system", "content": system_prompt},
            {"role": "user", "content": transcript},
        ],
    )

    # iterate over QuestionAnswer, add the `youtube_url`, and append to state
    for qna in response:
        qna.youtube_url = youtube_url
        # `State` is immutable, so `.append()` returns a new object with the appended value
        state = state.append(question_answers=qna)

    return state

2. 构建 Application

要创建 Burr Application,我们使用 ApplicationBuilder 对象。

至少需要

  • 使用 .with_actions() 定义所有可能的 actions。只需传递用 @action 装饰的函数。
  • 使用 .with_transitions() 定义 actions 之间的可能转换。这是通过元组 (from_action, to_action) 完成的。
  • 使用 .with_entrypoint() 指定首先运行哪个 action。
from burr.core import ApplicationBuilder

app = (
    ApplicationBuilder()
    .with_actions(
        process_user_input,
        get_youtube_transcript,
        generate_question_and_answers,
    )
    .with_transitions(
        ("process_user_input", "get_youtube_transcript"),
        ("get_youtube_transcript", "generate_question_and_answers"),
        ("generate_question_and_answers", "process_user_input"),
    )
    .with_entrypoint("process_user_input")
    .build()
)
app.visualize()

Burr application graph

你随时可以可视化应用图以了解逻辑流。

3. 启动应用

使用 Application.run() 将使应用执行 actions 直到达到停止条件。在这种情况下,我们在 process_user_input 之前停止,以便从用户获取 YouTube URL。

方法 .run() 返回一个元组 (action_name, result, state)。在这种情况下,我们只使用 state 来检查生成的问答对。

action_name, result, state = app.run(
    halt_before=["process_user_input"],
    inputs={"user_input": "https://www.youtube.com/watch?v=hqutVJyd3TI"},
)
print(state["question_answers"][0])

你可以在 while 循环中使用 .run() 创建一个简单的本地体验

while True:
    user_input = input("Enter a YouTube URL (q to quit): ")
    if user_input.lower() == "q":
        break

    action_name, result, state = app.run(
        halt_before=["process_user_input"],
        inputs={"user_input": user_input},
    )
    print(f"{len(state['question_answers'])} question-answer pairs generated")

下一步

既然你了解了如何使用 Instructor 获取可靠的 LLM 输出以及使用 Burr 构建应用结构,那么根据你的目标,有很多途径可以探索!

1. 构建复杂智能体

Instructor 通过提供结构来改进 LLM 的推理能力。嵌套模型和添加约束可以在几行代码中实现 获取带引用的事实提取知识图谱。此外,重试 使 LLM 能够自我纠正。

Burr 定义了用户、LLMs 和系统其余部分之间的边界。你可以在转换中添加 Condition 来创建易于理解的复杂工作流。

2. 将 Burr 添加到你的产品中

你的 Burr Application 是一个轻量级的 Python 对象。你可以在 notebook、脚本、Web 应用(如 Streamlit, Gradio 等)或作为 Web 服务(例如 FastAPI)中运行它。

ApplicationBuilder 提供了许多用于将应用生产化的特性

  • 持久化:保存和恢复 State(例如,存储对话历史记录)
  • 可观测性:记录和监控应用遥测数据(例如,LLM 调用、使用的 token 数量、错误和重试)
  • 流式处理和异步:通过流式传输 LLM 响应和异步运行 actions 来创建流畅的用户界面。

例如,你可以用几行代码将遥测数据记录到 Burr UI 中。首先,对 OpenAI 库进行插桩。然后,在 ApplicationBuilder 中添加 .with_tracker(),指定项目名称并启用 use_otel_tracing=True

from burr.core import ApplicationBuilder
from opentelemetry.instrumentation.openai import OpenAIApiInstrumentor

# instrument before importing instructor or creating the OpenAI client
OpenAIApiInstrumentor().instrument()

app = (
    ApplicationBuilder()
    .with_actions(
        process_user_input,
        get_youtube_transcript,
        generate_question_and_answers,
    )
    .with_transitions(
        ("process_user_input", "get_youtube_transcript"),
        ("get_youtube_transcript", "generate_question_and_answers"),
        ("generate_question_and_answers", "process_user_input"),
    )
    .with_tracker(project="youtube-qna", use_otel_tracing=True)
    .with_entrypoint("process_user_input")
    .build()
)

telemetry

使用 Instructor 进行 OpenAI API 调用的遥测数据。我们看到提示、响应模型和响应内容。

3. 标注应用日志

Burr UI 内置了标注工具,允许你对记录的数据(例如,用户输入、LLM 响应、为 RAG 检索的内容)进行标注、评分或评论。这对于创建测试用例和评估数据集非常有用。

annotation tool

结论

我们展示了 Instructor 如何帮助从 LLM 获取可靠的输出,以及 Burr 如何提供构建应用的正确工具。现在轮到你开始构建了!