0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Claudeがフィールド順を入れ替えてくるときのJSONストリームパーサ

0
Posted at

前提

サジェストは、次の形のJSONオブジェクトとしてストリーミングで返ってくる。

{"type": "...", "title": "...", "content": "..."}

画面に逐次出したいのは content の本文だけだ。type と title はメタ情報として扱う。

プロバイダごとの差

OpenAI と Google には response_format があり、json_schema を渡せる。出力の形をAPI側で縛れる。

Anthropic は事情が異なる。Anthropic の前段に置いた OpenAI 互換レイヤーは、response_format を受け取っても無視する。Anthropic には response_format 自体が存在しない。そのため、JSONの形はシステムプロンプト内の指示として書くことになる。

指示で形を伝えているだけなので、キーの並び順まで固定できるわけではない。実際、Claude は type より先に content を出力することがある。

{"content": "...", "type": "...", "title": "..."}

素朴なパーサで起きたこと

最初の実装は、届いたトークンをそのまま表示側へ流していた。キーの順序が想定どおりなら、それらしく見える。順序が崩れると、content の値が閉じた後の ", "type": " のようなJSONの残りがユーザーに見えるテキストとして画面に出てしまう。

どのトークンが値の中身で、どのトークンが構文なのかを、パーサ側で判定する必要がある。

状態機械とリザーブ

修正後は、状態機械でJSONを読み進める。いまキーを読んでいるのか値を読んでいるのか、値ならどのフィールドのものかを追跡する。表示に回すのは content の値の中身だけになる。

さらに、表示へ渡す前にキューの一部をリザーブとして手元に保持する。リザーブは、20文字たまるか100msが経過した時点でフラッシュする。トークン単位で細切れに描画せず、ある程度まとまった単位で画面を更新できる。

type Emit = (text: string) => void;

class ContentReserve {
  private reserve = "";
  private timer: ReturnType<typeof setTimeout> | null = null;

  constructor(private readonly emit: Emit) {}

  push(chunk: string): void {
    this.reserve += chunk;
    if (this.reserve.length >= 20) {
      this.flush();
      return;
    }
    if (this.timer === null) {
      this.timer = setTimeout(() => this.flush(), 100);
    }
  }

  flush(): void {
    if (this.timer !== null) {
      clearTimeout(this.timer);
      this.timer = null;
    }
    if (!this.reserve) return;
    this.emit(this.reserve);
    this.reserve = "";
  }
}

push に渡すのは、状態機械が content の値の内側だと判定した文字列だけだ。構文の文字はここに入らない。

欠けたフィールドの扱い

キー順が固定されないので、表示を始める時点で揃っていないフィールドが出てくる。

  • type がストリーム開始時点で未着の場合は、finalize のときに埋める。
  • title は、content が終わった後に届くことがある。

表示側は、content が先に完結し、メタ情報が後から確定するケースを前提に組んでおく。content の描画を type や title の到着待ちでブロックしない。

まとめ

response_format で形を縛れるプロバイダと、プロンプトの指示に頼るしかないプロバイダが混在する場合、キー順を前提にしたパーサはどこかで壊れる。状態機械で値の内側だけを取り出し、リザーブを20文字か100msでフラッシュし、欠けたフィールドは finalize で補う。この構成で、順序が入れ替わってもJSONの残りが画面に出なくなった。リアルタイムでサジェストを表示する LiveSuggest のストリーミング処理は、この形で動いている。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?