关键字匹配,状态机筛选流式输出内容

大模型输出的是json,比如{[{'title':'sdfasdfas', 'content': 'adfadsfaewfe'}, {'title':'adsfasewf', 'content': 'fwefwefweafewf'}, {'title':'asdfasdfdasf', 'content': 'feawfewfewf'}]},但我只流式yeild title, content 中的内容

from openai import AsyncOpenAI
import asyncio

client = AsyncOpenAI()

async def stream_and_extract():
    stream = await client.chat.completions.create(
        model="gpt-4o-mini",
        messages=[
            {
                "role": "user",
                "content": "请以 JSON 输出 items,每个包含 title 和 content"
            }
        ],
        stream=True,
    )

    target_keys = {"title", "content"}

    buffer = ""
    in_string = False
    current_key = None
    collecting = False

    async for chunk in stream:
        delta = chunk.choices[0].delta
        if not delta or not delta.content:
            continue

        text = delta.content

        for ch in text:
            buffer += ch

            # 1️⃣ 判断字符串边界
            if ch == '"' and not buffer.endswith('\\"'):
                in_string = not in_string

                # 字符串结束
                if not in_string and collecting:
                    print()          # 一个 value 结束
                    collecting = False
                    current_key = None
                continue

            if not in_string:
                continue

            # 2️⃣ 检测 key
            if buffer.endswith('"title"'):
                current_key = "title"
                continue
            if buffer.endswith('"content"'):
                current_key = "content"
                continue

            # 3️⃣ 检测 value 起点
            if current_key and buffer.endswith(':"'):
                collecting = True
                continue

            # 4️⃣ 真正输出 value
            if collecting:
                print(ch, end="", flush=True)

asyncio.run(stream_and_extract())

是的,这段代码逻辑上确实有问题,而且不是小问题 👍
你问得非常到位——这正是流式 JSON 状态机最容易踩坑的地方

我分三层来讲:

1️⃣ 明确告诉你:哪些地方是错的 / 不可靠的
2️⃣ 为什么会错(在真实 streaming 场景下一定会出问题)
3️⃣ 给你一版“修正后、能扛真实模型输出”的写法


一、先给结论(直说)

❌ 这段代码 在理想情况下“可能看起来能跑”
在真实 LLM token 流下会出现:

  • 漏字符
  • 误判 key
  • 提前 / 延后结束 value
  • 遇到转义字符直接乱套

👉 根因
你现在用的是「字符串后缀匹配 + 单字符状态切换」,但 JSON 的 token 边界和语义边界是不对齐的


二、逐条指出“明确的问题点”

❌ 问题 1:buffer.endswith('"title"') 不可靠

if buffer.endswith('"title"'):
    current_key = "title"

为什么错?

在真实流式里,token 可能是:

"ti
tle"

或者:

"title
"

👉 你永远不能假设 key 一次性完整出现

📉 结果:

  • current_key 可能永远设不上
  • 或在错误位置设上

❌ 问题 2:buffer.endswith(':"') 极其危险

if current_key and buffer.endswith(':"'):
    collecting = True

会直接翻车的场景:

"title" : "xxx"

注意空格!

"title":
"xxx"

注意换行!

"title":"xx\"x"

注意转义!

👉 JSON 允许任意空白字符,你现在是“硬编码格式假设”。


❌ 问题 3:字符串结束判断是错的

if ch == '"' and not buffer.endswith('\\"'):
    in_string = not in_string

这是经典 bug:

"abc\\\"def"

这里的 " 是被转义的,但:

buffer.endswith('\\"') == False

因为实际上结尾是 \\\"

👉 正确判断方式是:

  • 数一数前面连续的 \
  • 奇数 → 转义
  • 偶数 → 真结束

❌ 问题 4:buffer 无限增长(内存 & 逻辑问题)

buffer += ch
  • buffer 永远不清
  • 越来越慢
  • 后缀匹配越来越不可信

流式状态机里,buffer 只能是“短窗口”


❌ 问题 5:value 输出时包含结构字符的风险

if collecting:
    print(ch, end="", flush=True)

如果状态切换慢一拍,你会输出:

  • "
  • }
  • ,

而且你现在没有统一的 value 边界控制


三、为什么这类问题“必然发生”

LLM streaming 的最小单位是 token,不是 JSON 字段

一个 token 可能是:

"title":"a

或者:

":"con

👉 所以你不能靠:

  • endswith
  • 固定格式
  • 单字符切换

四、给你一版「修正后的、工业级可用」方案

✅ 核心改进思路

  • 不匹配 "title",而是:

    • 先完整解析 key token
  • 不靠 endswith

  • 严格区分三种状态:

    1. 解析 key
    2. 等待 value 起点
    3. 收集 value
  • 正确处理转义字符

  • 有限 buffer


🔥 改进版示例(仍然是轻量状态机)

from openai import AsyncOpenAI
import asyncio

client = AsyncOpenAI()

async def stream_and_extract():
    stream = await client.chat.completions.create(
        model="gpt-4o-mini",
        messages=[{
            "role": "user",
            "content": "请以 JSON 输出 items,每个包含 title 和 content"
        }],
        stream=True,
    )

    target_keys = {"title", "content"}

    state = "idle"          # idle | key | colon | value
    key_buf = ""
    val_buf = ""
    current_key = None
    escape = False

    async for chunk in stream:
        delta = chunk.choices[0].delta
        if not delta or not delta.content:
            continue

        for ch in delta.content:

            # 处理字符串转义
            if escape:
                if state == "value":
                    print(ch, end="", flush=True)
                escape = False
                continue

            if ch == "\\":
                escape = True
                if state == "value":
                    print(ch, end="", flush=True)
                continue

            # 状态机
            if state == "idle":
                if ch == '"':
                    key_buf = ""
                    state = "key"

            elif state == "key":
                if ch == '"':
                    current_key = key_buf if key_buf in target_keys else None
                    state = "colon"
                else:
                    key_buf += ch

            elif state == "colon":
                if ch == '"' and current_key:
                    state = "value"
                elif ch == '"' and not current_key:
                    state = "idle"

            elif state == "value":
                if ch == '"':
                    print()      # 一个 value 结束
                    state = "idle"
                    current_key = None
                else:
                    print(ch, end="", flush=True)

asyncio.run(stream_and_extract())

五、这版代码解决了什么?

✅ key / value token 拆分
✅ 转义字符
✅ 任意空白 / 换行
✅ 不依赖 JSON 格式细节
✅ 真正“token-safe”


六、重要建议(如果你能改 prompt)

永远不要让模型在 streaming 时输出 JSON 给你解析

最优 prompt:

请逐条输出:
<item>
TITLE: ...
CONTENT: ...
</item>

那你可以直接:

if line.startswith("TITLE:"):
    ...

JSON streaming 解析 = 最后的下策


七、总结一句话

你原来的代码:思路对,但实现不安全
改进后:才是能抗真实 LLM streaming 的版本

如果你愿意,我可以:

  • 把这段逻辑封装成 async generator
  • 支持任意 key 列表
  • 加上单元测试(用真实 token 拆分)

你是准备 直接推给前端,还是 后端消费

posted @ 2026-02-01 17:14  X1OO  阅读(0)  评论(0)    收藏  举报