Use when writing acceptance criteria for a task - express each as an observable Given/When/Then that QA can execute, including negative cases
日本語の概要は準備中です。原文の説明を表示しています。
Use when a model is served online (FastAPI, BentoML) or scored in batch, when changing how a model artifact is loaded or versioned, or when adding drift, quality or prediction monitoring and a rollout plan for a new model
インストールする前に、エージェントに与えられる指示の中身を確認できます。
A model in production fails in ways a notebook never shows: the API re-implements preprocessing slightly differently, the artifact loaded is not the one evaluated, inputs drift away from the training data, or an async endpoint blocks on CPU work and the service stalls. Serving code is ordinary backend code with typed inputs, tests and a health check — plus a version on every prediction.
Core principle: serve the exact fitted pipeline you evaluated, identified by an immutable version, and log enough with every prediction to notice when the world stops looking like the training data.
| Need | Choose |
|---|---|
| Scores consumed on a schedule (daily campaign, nightly risk list) | batch scoring as a pipeline stage (data-pipeline-orchestration) |
| A decision inside a user request (fraud check, ranking) | online endpoint with a latency budget |
| Both | one fitted pipeline, two thin callers — never two feature implementations |
Batch is simpler to operate, test and roll back; choose online only when the decision needs it.
class Features(BaseModel):
model_config = ConfigDict(extra="forbid")
days_since_order: int = Field(ge=0, le=3650)
orders_90d: int = Field(ge=0)
plan: Literal["free", "pro", "team"]
class ScoreRequest(BaseModel):
model_config = ConfigDict(extra="forbid")
rows: list[Features] = Field(min_length=1, max_length=1000)
class ScoreResponse(BaseModel):
model_version: str
scores: list[float]
def create_app(settings: Settings) -> FastAPI:
@asynccontextmanager
async def lifespan(app: FastAPI):
app.state.model = load_model(settings.model_path, settings.trusted_types)
app.state.model_version = settings.model_version
yield
app = FastAPI(lifespan=lifespan)
@app.get("/health")
def health() -> dict[str, str]:
return {"status": "ok"}
@app.get("/ready")
def ready(request: Request) -> dict[str, str]:
if getattr(request.app.state, "model", None) is None:
raise HTTPException(status_code=503, detail="model not loaded")
return {"status": "ready", "model_version": request.app.state.model_version}
@app.post("/score")
def score(body: ScoreRequest, request: Request) -> ScoreResponse:
frame = pd.DataFrame([row.model_dump() for row in body.rows])
proba = request.app.state.model.predict_proba(frame)[:, 1]
return ScoreResponse(model_version=request.app.state.model_version, scores=proba.tolist())
return app
/health says the process is alive; /ready says the model is loaded.def, not async def, for CPU-bound inference: FastAPI runs sync endpoints in a thread pool; an async def calling predict_proba blocks the event loop for every other request.@bentoml.service classes) or another server is fine where the repository already uses it; the same rules apply (rule stack-choice).models:/churn@champion → version 12), or a content hash. Configuration names the version; the service logs it at startup.def test_score_matches_the_training_pipeline(model_settings, churn_fixture) -> None:
X, _ = churn_fixture
with TestClient(create_app(model_settings)) as client:
response = client.post("/score", json={"rows": X.head(3).to_dict("records")})
assert response.status_code == 200
expected = load_model(model_settings.model_path, model_settings.trusted_types).predict_proba(X.head(3))[:, 1]
np.testing.assert_allclose(response.json()["scores"], expected)
def test_score_rejects_an_unknown_plan(client) -> None:
response = client.post("/score", json={"rows": [{"days_since_order": 3, "orders_90d": 1, "plan": "gold"}]})
assert response.status_code == 422
conftest.py and saved to tmp_path — no registry, no network.TestClient used as a context manager runs the lifespan, so the load path is tested too./ready before and after load, an empty rows list and an oversized batch rejected, the version present in every response.def score_partition(day: date, model: Pipeline, model_version: str, out_root: Path) -> Path:
features = load_features(day)
scores = features[["customer_id"]].assign(
score=model.predict_proba(features)[:, 1],
model_version=model_version,
scored_for=day,
)
target = out_root / f"day={day:%Y-%m-%d}" / "scores.parquet"
target.parent.mkdir(parents=True, exist_ok=True)
tmp = target.with_suffix(".tmp")
scores.to_parquet(tmp, index=False)
tmp.replace(target)
return target
Idempotent per partition, written atomically, every row stamped with the model version. Validate the output before publishing it: no NaN scores, scores within [0, 1], one row per entity (data-validation-contracts).
Write the steps, the triggers and the way back into after_deploy and rollback_plan (rule deploy-runbook-fields): rolling back a model is pointing the alias or config back at the previous version — record which.
Log per prediction (or per batch): model version, timestamp, the input features or a hash of them, the score — within what the privacy policy allows; no raw PII the task did not approve. Then watch:
| Signal | How | Why |
|---|---|---|
| Input drift | PSI or a statistical test per feature against the training reference; Evidently where the repo uses it | the world changed |
| Prediction drift | score distribution vs the validation period | often the first visible symptom |
| Data quality | null rates, unseen categories, out-of-range values on live inputs | an upstream break |
| Performance | the real metric once labels arrive (often weeks later) | the only proof the model still works |
| Service | latency, error rate, readiness | it is still an API |
def psi(expected: np.ndarray, actual: np.ndarray, bins: int = 10) -> float:
edges = np.unique(np.quantile(expected, np.linspace(0, 1, bins + 1)))
e = np.histogram(np.clip(expected, edges[0], edges[-1]), edges)[0] / len(expected)
a = np.histogram(np.clip(actual, edges[0], edges[-1]), edges)[0] / len(actual)
e, a = np.clip(e, 1e-6, None), np.clip(a, 1e-6, None)
return float(np.sum((a - e) * np.log(a / e)))
A common reading: below 0.1 stable, 0.1–0.25 worth a look, above 0.25 a real shift. Thresholds and alerts are agreed with the people who own the decision; Evidently's API changed across versions — follow the pinned one.
async def endpoints doing CPU-bound inference.latest as the model reference in production configuration.まだレビューはありません。使ってみた感想をお寄せください。
概要と使いどころ
Use when writing acceptance criteria for a task - express each as an observable Given/When/Then that QA can execute, including negative cases
日本語の概要は準備中です。原文の説明を表示しています。
Use when the diff adds or changes an endpoint, resolver, RPC, job or query that takes an object id, a role check, a request binding or a tenant filter - BOLA/IDOR, function-level authorization, mass assignment and tenant scoping
日本語の概要は準備中です。原文の説明を表示しています。
Use on every UI change - semantic HTML, labels for controls, keyboard-navigable dialogs/menus, visible focus, and never color as the only signal
日本語の概要は準備中です。原文の説明を表示しています。
Use when a task changes any screen, form, dialog, menu or control - Lighthouse/axe scan of the changed screens, a keyboard walk, and the thresholds that fail a task
日本語の概要は準備中です。原文の説明を表示しています。
How to work a task returned with review, QA or UAT findings. Use when a task is in need_revision or PR review comments are in your context.
日本語の概要は準備中です。原文の説明を表示しています。
Use when deciding whether a request needs an analiz task before implementation - the conditions that require the architect's analysis versus going straight to implementation
日本語の概要は準備中です。原文の説明を表示しています。