{"id":16712,"date":"2026-07-17T00:00:00","date_gmt":"2026-07-17T07:00:00","guid":{"rendered":"https:\/\/devblogs.microsoft.com\/ise\/?p=16712"},"modified":"2026-07-17T08:52:35","modified_gmt":"2026-07-17T15:52:35","slug":"ai-asset-enrichment-pipeline","status":"publish","type":"post","link":"https:\/\/devblogs.microsoft.com\/ise\/ai-asset-enrichment-pipeline\/","title":{"rendered":"Teaching a Vision Model to See Like a Human Annotator\u2014and Catching It When It Lies"},"content":{"rendered":"<h1>Introduction: The Problem<\/h1>\n<p>Imagine a catalog of 50,000 visual assets\u2014characters, environments, props\u2014and no way to search them except scrolling. Someone asks: <em>&#8220;Find me all the night scenes with a warrior holding a weapon.&#8221;<\/em> Without structured metadata, that search is impossible.<\/p>\n<p>Manual tagging does not scale. We needed a way to <strong>automatically extract rich, structured metadata from images<\/strong>\u2014and critically, to <strong>trust<\/strong> the output enough to put it in a production search index.<\/p>\n<p>The core challenge: <strong>multimodal LLMs are excellent at describing images, but they hallucinate.<\/strong> They confidently report characters not in the scene and invent props that do not exist. For a search catalog, a hallucinated tag is worse than a missing one\u2014it erodes trust in the system.<\/p>\n<blockquote><p><strong>The question was not &#8220;can an LLM extract metadata from images?&#8221;\u2014it was &#8220;can we make the output reliable enough to trust at scale?&#8221;<\/strong><\/p><\/blockquote>\n<h2>Why Enrichment Instead of Just Embeddings?<\/h2>\n<table>\n<thead>\n<tr>\n<th>Approach<\/th>\n<th>What it enables<\/th>\n<th>What it cannot do<\/th>\n<th>Output<\/th>\n<th>What it means in practice<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td><strong>Embeddings<\/strong><\/td>\n<td>&#8220;Find images similar to this one&#8221;<\/td>\n<td>Tell you <em>why<\/em> they are similar, or filter by specific attributes<\/td>\n<td>Black-box vector<\/td>\n<td>It can rank similar images, but it cannot tell you that an image contains a male protagonist in an indoor environment at night. There is no structured output to filter on.<\/td>\n<\/tr>\n<tr>\n<td><strong>Enrichment<\/strong><\/td>\n<td>&#8220;Find all night scenes with a warrior holding a weapon&#8221;<\/td>\n<td>Handle fuzzy, open-ended similarity queries<\/td>\n<td>Structured, auditable fields<\/td>\n<td>It produces tags, character roles, environment type, and prop categories. Each field is a testable claim that can be filtered, validated, and indexed.<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<blockquote><p>Enrichment and embeddings are complementary. Enrichment enables faceted search and filtering. Embeddings enable semantic similarity. Use both.<\/p><\/blockquote>\n<h2>The Journey: Our Approach and Solution<\/h2>\n<h3>What the Output Actually Does<\/h3>\n<p>Before diving into how it works, here is what the enrichment pipeline produces. Given a scene image, the pipeline outputs structured JSON:<\/p>\n<pre><code class=\"language-json\">{\r\n  \"tags\": [\"palace\", \"night\", \"torch-lit\", \"warrior\", \"indoor\"],\r\n  \"environment\": {\r\n    \"type\": \"indoor\",\r\n    \"time_of_day\": \"night\",\r\n    \"lighting\": \"torch-lit\",\r\n    \"realism\": \"cgi\"\r\n  },\r\n  \"characters\": [\r\n    {\r\n      \"name\": \"Warrior A\",\r\n      \"role\": \"protagonist\",\r\n      \"gender\": \"male\",\r\n      \"species\": \"human\",\r\n      \"position\": \"center\",\r\n      \"action\": \"speaking\",\r\n      \"visible_in_image\": true\r\n    }\r\n  ],\r\n  \"props\": [\r\n    {\r\n      \"name\": \"ceremonial sword\",\r\n      \"category\": \"weapon\",\r\n      \"owner\": \"Warrior A\",\r\n      \"visible_in_image\": true\r\n    }\r\n  ]\r\n}<\/code><\/pre>\n<p>Every value in that JSON comes from a <strong>constrained set of options<\/strong>\u2014not free text. That design decision makes the entire pipeline evaluable, and it ties the rest of this post together.<\/p>\n<h3>The Flow<\/h3>\n<p>The following diagram shows the complete flow\u2014from input image through the pipeline to validated metadata:<\/p>\n<p><img decoding=\"async\" src=\"https:\/\/devblogs.microsoft.com\/ise\/wp-content\/uploads\/sites\/55\/2026\/07\/ai-asset-enrichment-pipeline.webp\" alt=\"Enrichment Pipeline: Image to Structured Metadata with Ground Truth Evaluation\" \/><\/p>\n<h3>The Pipeline at a Glance<\/h3>\n<p><img decoding=\"async\" src=\"https:\/\/devblogs.microsoft.com\/ise\/wp-content\/uploads\/sites\/55\/2026\/07\/pipeline-at-a-glance.webp\" alt=\"Pipeline at a Glance: Image to Guard to Prompt Assembly to GPT-4.1 Vision to 10-Stage Validator to Enriched Metadata\" \/><\/p>\n<h3>Step 1: The Image-First Principle<\/h3>\n<p>When a production team provides context\u2014<em>&#8220;this scene has characters A, B, and C&#8221;<\/em>\u2014the model is tempted to report all three even if only one appears. Context biases the model toward expected content rather than observed content.<\/p>\n<blockquote><p><strong>Rule: The final image is ground truth.<\/strong> Reference images and text help the model <em>identify and name<\/em> elements\u2014but only elements it can actually <em>see<\/em>. Everything else must be marked <code>visible_in_image=false<\/code>.<\/p><\/blockquote>\n<p>Think of it like a witness in court\u2014background knowledge helps identify people, but testimony must cover only what was personally observed.<\/p>\n<h3>Step 2: Preprocessing for Accuracy<\/h3>\n<ul>\n<li><strong>Resize to 1,024px<\/strong> (preserving aspect ratio)\u2014balances fidelity against token cost<\/li>\n<li><strong>LANCZOS resampling<\/strong>\u2014preserves fine details like text on props and facial features<\/li>\n<li><strong>Message ordering<\/strong>\u2014reference images first, final asset last, then the prompt<\/li>\n<\/ul>\n<h3>Step 3: Constrained Output\u2014The Key Design Decision<\/h3>\n<p>Instead of asking the model <em>&#8220;describe the environment&#8221;<\/em> (free text, impossible to evaluate), the pipeline asks <em>&#8220;classify the environment into one of these categories&#8221;<\/em>:<\/p>\n<p><img decoding=\"async\" src=\"https:\/\/devblogs.microsoft.com\/ise\/wp-content\/uploads\/sites\/55\/2026\/07\/constrained-output-schema-scaled.webp\" alt=\"Constrained Output Schema: Vision Model branches into Environment, Characters, Props, and Tags with enumerated values\" \/><\/p>\n<p><strong>Why this matters:<\/strong><\/p>\n<table>\n<thead>\n<tr>\n<th>Free-text output<\/th>\n<th>Constrained output<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td><em>&#8220;The scene appears to take place during the evening hours, possibly dusk or early night&#8221;<\/em><\/td>\n<td><code>time_of_day: \"night\"<\/code><\/td>\n<\/tr>\n<tr>\n<td>Requires subjective judgment or another LLM to evaluate<\/td>\n<td>Simple classification check: <strong>right or wrong<\/strong><\/td>\n<\/tr>\n<tr>\n<td>Model can invent any value<\/td>\n<td>Model must pick from a known set<\/td>\n<\/tr>\n<tr>\n<td>Impossible to compute precision\/recall<\/td>\n<td>Standard classification metrics apply directly<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<p>The model&#8217;s output is enforced via <strong>JSON schema mode<\/strong> (<code>strict: true<\/code>). If the model produces an unlisted value, the validator coerces it to <code>unknown<\/code> rather than propagating noise.<\/p>\n<blockquote><p><strong>The <code>unknown<\/code> fallback is deliberate.<\/strong> It preserves the record without asserting false information. A system that says &#8220;I don&#8217;t know&#8221; is more trustworthy than one that guesses.<\/p><\/blockquote>\n<h3>Step 4: The 10-Stage Hallucination Defense<\/h3>\n<p>Raw model output\u2014even with structured JSON enforced\u2014is never trusted directly. A <strong>10-stage validation pipeline<\/strong> acts as an independent safety net:<\/p>\n<p><img decoding=\"async\" src=\"https:\/\/devblogs.microsoft.com\/ise\/wp-content\/uploads\/sites\/55\/2026\/07\/10-stage-validation-scaled.webp\" alt=\"10-Stage Validation Pipeline: Parse and Normalize, Hallucination Guard with visibility filters, and Assemble with Ground Truth Evaluation\" \/><\/p>\n<p><strong>Stages 6, 7, and 8 are the hallucination guards<\/strong>\u2014they unconditionally remove any character or prop marked <code>visible_in_image=false<\/code> and any unnamed prop. The model cannot override these filters.<\/p>\n<ul>\n<li><strong>Prompt layer (soft):<\/strong> The model is instructed to be honest about visibility\u2014this handles most cases correctly.<\/li>\n<li><strong>Validator layer (hard):<\/strong> Anything marked <code>false<\/code> is removed. Period. This rule catches cases where context bias overrode visual evidence.<\/li>\n<\/ul>\n<p>Items marked <code>null<\/code> (uncertain) are <strong>kept<\/strong>\u2014the pipeline prefers to retain uncertain data rather than silently discard it. Every removal is logged for auditability.<\/p>\n<blockquote><p><strong>Analogy:<\/strong> The prompt layer is like training an employee to be accurate. The validator layer is like having a supervisor check every report. You need both.<\/p><\/blockquote>\n<h3>Step 5: Deterministic Description Generation<\/h3>\n<p>The human-readable descriptions (<em>&#8220;A torch-lit indoor palace at night with a warrior protagonist&#8221;<\/em>) are <strong>not generated by the LLM<\/strong>. They are composed deterministically from the validated structured fields\u2014so descriptions always match the structured data. No contradictions, no drift.<\/p>\n<h2>Evaluating the Pipeline: Ground Truth Template<\/h2>\n<p>Because every output field is constrained to a finite set of values, evaluation becomes a <strong>classification problem<\/strong>\u2014not a subjective comparison. We proposed a ground truth evaluation template:<\/p>\n<table>\n<thead>\n<tr>\n<th>What to evaluate<\/th>\n<th>How to measure it<\/th>\n<th>Why it works<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td>Environment type, time of day, lighting<\/td>\n<td><strong>Exact match<\/strong> against human annotation<\/td>\n<td>Enum \u2192 simple right\/wrong check<\/td>\n<\/tr>\n<tr>\n<td>Character presence and roles<\/td>\n<td><strong>Precision and recall<\/strong><\/td>\n<td>Did the model find all characters? Did it add extras?<\/td>\n<\/tr>\n<tr>\n<td>Prop identification and categories<\/td>\n<td><strong>Precision and recall<\/strong><\/td>\n<td>Same as characters, for props<\/td>\n<\/tr>\n<tr>\n<td>Tag quality<\/td>\n<td><strong>Overlap score<\/strong> (Jaccard similarity)<\/td>\n<td>How much do generated tags overlap with human tags?<\/td>\n<\/tr>\n<tr>\n<td>Visibility correctness<\/td>\n<td><strong>Binary accuracy<\/strong> on <code>visible_in_image<\/code><\/td>\n<td>The hallucination-specific metric<\/td>\n<\/tr>\n<tr>\n<td>Overall enrichment quality<\/td>\n<td><strong>Ground truth template comparison<\/strong><\/td>\n<td>Compare full output against human-annotated expected output<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<p>The template also captures <strong>edge cases<\/strong>: images with no characters (expect an empty list), ambiguous lighting (expect <code>unknown<\/code>), partially occluded props.<\/p>\n<blockquote><p><strong>Key insight:<\/strong> Constrained output makes evaluation <em>tractable<\/em>. Enum-constrained evaluation is a standard classification problem.<\/p><\/blockquote>\n<h2>Why a Plug-and-Play Module?<\/h2>\n<p>The enrichment pipeline was designed to be <strong>self-contained and portable<\/strong>:<\/p>\n<ul>\n<li><strong>One abstract interface<\/strong>\u2014The pipeline depends on a <code>VisionExtractionClient<\/code> with a single <code>extract()<\/code> method. Swapping model providers is a configuration change, not a code change.<\/li>\n<li><strong>Isolated configuration<\/strong>\u2014All enrichment settings live in a dedicated config. The module has <strong>zero knowledge<\/strong> of other modules.<\/li>\n<li><strong>Self-contained contracts<\/strong>\u2014The module defines its own input\/output schemas. Any system that sends a request and consumes the response can use it.<\/li>\n<li><strong>Optional by design<\/strong>\u2014Enable or disable enrichment at runtime. No errors when disabled.<\/li>\n<\/ul>\n<blockquote><p><strong>Practical implication:<\/strong> The same module runs in the production API, batch scripts, and evaluation notebooks\u2014without code changes.<\/p><\/blockquote>\n<h2>The Destination: Outcomes and Learnings<\/h2>\n<h3>Search Quality Impact<\/h3>\n<p>Enrichment improved search relevance measurably. The table below compares average cosine similarity scores between user queries and catalog results, with and without enrichment:<\/p>\n<table>\n<thead>\n<tr>\n<th>Comparison<\/th>\n<th>Without Enrichment<\/th>\n<th>With Enrichment<\/th>\n<th>Gain<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td>Query vs. Prompt (text only)<\/td>\n<td>0.50<\/td>\n<td>0.61<\/td>\n<td>+22.5%<\/td>\n<\/tr>\n<tr>\n<td>Query vs. Prompt + Image (combined)<\/td>\n<td>0.49<\/td>\n<td>0.59<\/td>\n<td>+20.3%<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<p>For assets that included reference images, the gains were even larger:<\/p>\n<table>\n<thead>\n<tr>\n<th>Comparison<\/th>\n<th>Without Enrichment<\/th>\n<th>With Enrichment<\/th>\n<th>Gain<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td>Query vs. Prompt (text only)<\/td>\n<td>0.46<\/td>\n<td>0.61<\/td>\n<td>+32.7%<\/td>\n<\/tr>\n<tr>\n<td>Query vs. Prompt + Image (combined)<\/td>\n<td>0.46<\/td>\n<td>0.59<\/td>\n<td>+29.3%<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<p>Reference images gave the model more visual context to work with, which translated directly into richer, more accurate metadata and stronger search alignment.<\/p>\n<h3>Learnings<\/h3>\n<ol>\n<li><strong>Constrained output was the highest-leverage decision.<\/strong> It simultaneously minimized hallucination (model cannot invent values), enabled evaluation (classification metrics, not subjective judgment), and improved downstream search quality (clean, consistent facets).<\/li>\n<li><strong>Two layers of hallucination defense caught different failure modes.<\/strong> The prompt layer handled most cases; the validator caught the remaining ~5-10% where context bias overrode visual evidence. Neither alone was sufficient.<\/li>\n<li><strong>Deterministic descriptions eliminated a hallucination surface<\/strong> by not asking the model for free text.<\/li>\n<li><strong>The plug-and-play design paid off immediately.<\/strong> The same module ran in the production API, batch backfill scripts, and evaluation notebooks\u2014without modification.<\/li>\n<li><strong>Tracking <code>unknown<\/code> rates revealed blind spots early.<\/strong> A high <code>unknown<\/code> rate on a field signals the model consistently struggles\u2014that field may need few-shot examples or a specialized classifier.<\/li>\n<\/ol>\n<h3>The Broader Pattern<\/h3>\n<p>These techniques apply wherever an LLM produces structured output for production: medical record extraction, document classification, catalog enrichment, e-commerce product tagging.<\/p>\n<p>While this pipeline focused on <strong>static images with text context<\/strong>, the same constrained-output-plus-validation pattern extends naturally to other modalities. You can process video assets by sampling keyframes, enriching each frame independently, and then merging results across the timeline. Audio and 3D assets follow the same principle\u2014extract structured metadata via a capable model, constrain it to known values, and validate before trusting.<\/p>\n<p>The core principle: <strong>do not treat the LLM as a source of truth. Treat it as a first draft that must be validated, constrained, and evaluated against known-good data.<\/strong><\/p>\n<h2>Thanks<\/h2>\n<p>A special thanks to <a href=\"https:\/\/www.linkedin.com\/in\/krammai\">Mark D&#8217;Souza<\/a>, <a href=\"https:\/\/www.linkedin.com\/in\/prabaldeb\">Prabal Deb<\/a>, <a href=\"https:\/\/www.linkedin.com\/in\/preethi-natarajan-\">Preethi Natarajan<\/a>, <a href=\"https:\/\/www.linkedin.com\/in\/bhimjyani\">Priya Bhimjyani<\/a> for their valuable input.<\/p>\n<blockquote><p>The feature image was generated using Copilot. Terms can be found at <a href=\"https:\/\/www.microsoft.com\/en-in\/microsoft-copilot\/for-individuals\/termsofuse\">Microsoft Copilot Terms of Use<\/a>.<\/p><\/blockquote>\n","protected":false},"excerpt":{"rendered":"<p>A multimodal LLM enrichment pipeline that extracts structured metadata from visual assets, constrains output to predefined values to minimize hallucinations, and uses a ground truth evaluation template to measure quality\u2014all as a plug-and-play module.<\/p>\n","protected":false},"author":212371,"featured_media":16713,"comment_status":"closed","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"_acf_changed":false,"footnotes":""},"categories":[14,1,19],"tags":[3400],"class_list":["post-16712","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-cognitive-services","category-cse","category-machine-learning","tag-ise"],"acf":[],"blog_post_summary":"<p>A multimodal LLM enrichment pipeline that extracts structured metadata from visual assets, constrains output to predefined values to minimize hallucinations, and uses a ground truth evaluation template to measure quality\u2014all as a plug-and-play module.<\/p>\n","_links":{"self":[{"href":"https:\/\/devblogs.microsoft.com\/ise\/wp-json\/wp\/v2\/posts\/16712","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/devblogs.microsoft.com\/ise\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/devblogs.microsoft.com\/ise\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/ise\/wp-json\/wp\/v2\/users\/212371"}],"replies":[{"embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/ise\/wp-json\/wp\/v2\/comments?post=16712"}],"version-history":[{"count":1,"href":"https:\/\/devblogs.microsoft.com\/ise\/wp-json\/wp\/v2\/posts\/16712\/revisions"}],"predecessor-version":[{"id":16714,"href":"https:\/\/devblogs.microsoft.com\/ise\/wp-json\/wp\/v2\/posts\/16712\/revisions\/16714"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/ise\/wp-json\/wp\/v2\/media\/16713"}],"wp:attachment":[{"href":"https:\/\/devblogs.microsoft.com\/ise\/wp-json\/wp\/v2\/media?parent=16712"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/ise\/wp-json\/wp\/v2\/categories?post=16712"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/ise\/wp-json\/wp\/v2\/tags?post=16712"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}