跳到正文
DEV Community· StarGate·· 2 小時前AI 評分63

StarGate 的 Market Opinion 管線如何每日以 22 種語言發布 LLM 市場簡報並防止模型捏造事實

Publishing a daily LLM market note in 22 languages without letting it invent facts

AI 導讀

StarGate 的 Market Opinion 管線每日為 BTC、ETH、GRAM 及 DOGE 發布 22 種語言的市場簡報,並透過證據校驗和發布檢查防止模型捏造事實。

正文

Every evening our pipeline publishes one market note for BTC, ETH, GRAM and DOGE: what moved, what drove it, and three scenarios for the next 24 hours with probabilities. It has run daily since September 25, 2026: 39 issues so far, none withdrawn. The interesting part is not the prompt. It is everything around the model that decides whether a draft is allowed to reach readers. Here is that part.

The rule: two authorities for facts

A market note mixes two kinds of facts, and we never let one prove the other:

Claim Authority Example
Something happened sourceEvidence: articles from an allow-list of public feeds "Binance began the second withdrawal phase"
A number marketEvidence: exchange quotes read at writing time, with observedAt "BTC $77,592.96, +0.851% in 24 h"

The writer and the reviewer receive the same two evidence sets. A news article that mentions a price does not make that price true, and a market quote does not prove an event. Early on we required publisher articles to repeat exchange numbers, and the reviewer rejected perfectly good drafts as review_unsupported_market_data. Splitting the authorities removed those false rejections without loosening anything.

Sources are an allow-list

Six feeds today: Cointelegraph, Decrypt, the Ethereum Foundation blog, SEC press releases and two Federal Reserve feeds. When a feed started answering our fetcher with a Cloudflare challenge, we disabled the source instead of solving the challenge: solving it would be circumvention, not fetching. Ingestion only stores claimless references; no model call happens per article.

Strict structured output, and its traps

Every stage returns JSON against a strict schema. Two lessons:

  • The provider's strict subset is narrower than JSON Schema. A schema with uniqueItems was refused with 400 invalid_json_schema before the model was reached, so extraction failed on every run while other stages passed. A unit test now scans every stage schema for keywords the subset rejects and fails with the stage and keyword.
  • Some invariants cannot live in the schema. "Probabilities total 100" or "eventAt is ISO-8601 or null" are stated in the instruction and checked in code after parsing. Leaving them only in code meant paying for a schema-valid answer and then discarding it.

Fail closed, and say why

A draft passes deterministic checks (citations point at the provided sources, numbers match marketEvidence, probabilities sum to 100, required sections exist) and then an independent review step. Anything else is not published.

The first version refused candidates with a generic content_validation_failed, and production could not tell which of seventeen checks fired. Now every refusal is a typed reason (an enum case such as citation_outside_cluster) stored on the claim row. We deliberately do not log the exception message: a fault raised over article text or a prompt can carry that text, so logs carry only a bounded class-name chain of the fault.

Translations: same facts, new words

The English issue is published first. Then each of the 21 other locales gets its own job. Application code copies the canonical fields (numbers, probabilities, citation URLs, asset keys) into every language; the model only translates prose. One call for thirteen locales once produced 11,484 of 12,000 output tokens in 119.5 s against a 120 s timeout, so translation now runs in separate calls instead of one giant one.

A deterministic normalizer runs before storage: GRAM is the current name of the coin formerly called Toncoin, and sources still use the old ticker. Whole-word TON and Toncoin in model-written text become GRAM; URLs stay untouched.

A hard daily budget

There is one money limit: a fixed daily ceiling. Before every paid call the gateway computes a provable input-token ceiling and reserves input ceiling × input rate + output cap × output rate. Nothing is sent unless the reservation succeeds. When a response carries exact usage, the reservation shrinks to the actual cost before the next call. A definitive 4xx is settled at actual usage (no inference ran); a 5xx keeps the worst case, because it can fail after inference started.

The bug that taught us this: a flag meant "a paid call happened in this run" and was never cleared, so any later failure was billed as uncertain worst case. One broken run consumed the whole day's budget with zero publications, two days in a row. The flag is now "a paid call is in flight" and is cleared only after exact usage is confirmed.

Delivery is not part of generation

Publishing writes the document; delivery is a separate rail. The site renders the document in the reader's locale, an RSS feed exists per locale, and a bot digest is queued as its own job after translations arrive (or after a two-hour wait, falling back to English). Views are counted once per visitor per day by the browser after the article is on screen, not by the server-side render, so crawlers do not inflate them.

Checklist

  • [ ] separate the authority for events from the authority for numbers
  • [ ] allow-list sources; disable a source that blocks you, do not bypass it
  • [ ] test your schemas against the provider's strict subset
  • [ ] state cross-field invariants in the instruction and check them in code
  • [ ] store a typed refusal reason per draft; never log raw content
  • [ ] copy numbers and links in code; let the model translate only prose
  • [ ] reserve the worst case before each call; shrink to actual usage after it

Disclosure: we build StarGate, whose Market Opinion is the pipeline described here. This post was drafted by an AI assistant from the pipeline's code and documentation; the numbers come from its production history.

來源:DEV Community · dev.to