Constraining LLM Responses to Governed Structure and Content
How an LLM can interpret ambiguous human intent and return meaningful, usable data within a deterministic system.
Enterprise AI adoption is increasingly constrained less by access to models than by an organization's ability to make its business semantics, data, workflows, and system interfaces trustworthy enough for models to operate against. The architectural challenge is to reconcile meaning across heterogeneous systems, encode accepted meanings as reusable metadata, contracts, and rules, enforce deterministic behavior at system boundaries where practical, and confine probabilistic AI to explicitly bounded interpretation and reasoning tasks.
LiberAnalytics currently requires users to express post searches through predefined categories, tags, and other database parameters. Humans, however, do not naturally communicate in database parameters. This article explores whether an LLM can serve as a constrained semantic layer, translating natural-language search intent into validated parameters that can be safely consumed by LiberAnalytics' existing deterministic database orchestration.
Constraining LLM Output with a Database-Derived Schema
In implementing an LLM-based semantic search the question that must first be posed is: what is the LLM permitted to return as valid results from the user's request? If an LLM has the ability to interpret and generate human language from a given prompt, then the responsibility of the application calling the LLM is to define the set of responses permitted and the permissible output contract. Using an application's authoritative domain data to constrain an LLM's response to a prompt is one way to constrain responses to the application's authoritative domain and prevent the model from returning out-of-domain values.
LiberAnalytics LLM semantic search limits what the LLM can return from a user request by making the existing authoritative post categories and post tags the only valid domain values for those respective response properties. In the below snippet, the 'required' is telling the LLM that the postCategory and postTags must be returned, even if the values are null or an empty array respectively. The 'additionalProperties' => false instructs the LLM that the returned object may not contain properties other than the ones defined under properties.
// Define the domain of acceptable response values
// and the structured output schema.
$semanticSearchSchema = [
'type' => 'object' ,
'properties' => [
// Define the type and value for post category that
// can be returned, including null.
'postCategory' => [
'type' => [ 'string' , 'null' ],
'enum' => array_merge ( $postCategories , [ null ])
],
// Define the type and set of post tag values that
// can be returned.
'postTags' => [
'type' => 'array' ,
'items' => [
'type' => 'string' ,
'enum' => $postTags
]
]
],
// Returned objects must contain the postCategory
// and postTags properties.
'required' => [
'postCategory' ,
'postTags'
],
// Don't return any other properties.
'additionalProperties' => false
];
We can think of the above schema as four distinct sections:
- type - defines the permitted JSON type for an object, property, or array item
- enum - defines the specific values permitted
- required - defines which properties must exist
- additionalProperties - defines whether undeclared properties are allowed
Defining the LLM’s Role and Output Contract
The LLM’s role is defined in the instructions: identify the user’s primary subject as a post category and narrower concepts, technologies, or methods as post tags. The instructions also define the no-match behavior, allowing the model to return null for postCategory and an empty array for postTags when the user’s request does not map clearly to the LiberAnalytics domain. The input is simply the user’s submitted search prompt.
The more consequential part of the request is the text.format definition. Rather than allowing free-form text or arbitrary JSON, the response is required to conform to a JSON Schema named semantic_search. With strict enabled, the model’s output must adhere to the structure and permitted values defined by $semanticSearchSchema.
// Define OpenAI responses api parameters.
$openAiRequest = [
// Define the OpenAI model used for semantic interpretation.
'model' => 'gpt-5.6-luna' ,
// Guide how the LLM should interpret the user's search prompt
// within LiberAnalytics' allowed semantic domain, including
// the expected response behavior when no clear match exists.
'instructions' =>
'Interpret the user search request using only the supplied LiberAnalytics domain. ' .
'Treat postCategory as the primary subject or broad topic of the request. ' .
'Choose a postCategory only when the request clearly centers on one of the allowed categories. ' .
'Treat postTags as narrower concepts, technologies, methods, or attributes mentioned or clearly implied by the request. ' .
'Be conservative. If there is no clear semantic match, return null for postCategory and an empty array for postTags.' ,
// Supply the user's submitted search prompt.
'input' => $userSearchPromptRequest ,
// Define the model's text output as structured JSON using a
// JSON Schema named semantic_search, with strict adherence to
// the $semanticSearchSchema contract.
'text' => [
'format' => [
'type' => 'json_schema' ,
'name' => 'semantic_search' ,
'strict' => true ,
'schema' => $semanticSearchSchema
]
]
];
The Responses API parameters can be summarized as:
- model - defines the OpenAI model used for interpretation
- instructions - guides how the model should interpret the user's search prompt
- input - supplies the user's natural-language search prompt
- text.format - defines the structured output contract
- text.format.type - output as structured JSON according to the supplied schema
- text.format.name - identifies the structured-output format for this schema definition
- text.format.strict - requires the response to conform to the supplied schema
- text.format.schema - supplies LiberAnalytics' database-derived response constraints
Orchestrating the OpenAI Responses API Request
Calling the Responses API is a matter of sending an HTTP POST request to the OpenAI Responses API endpoint, supplying the previously defined request parameters as a JSON payload and the API key as a Bearer token in the request headers.
// Define the OpenAI Responses API call body, url, and key file.
$requestBody = json_encode ( $openAiRequest );
$curl = curl_init ( 'https://api.openai.com/v1/responses' );
$openAiKeyFile = $laConfig -> getConfigAttr ( 'liberanalytics' , 'config' , 'env' , 'openapikey' );
$openAiApiKey = trim ( file_get_contents ( $openAiKeyFile ));
// Set curl session options, supply the request body.
curl_setopt_array ( $curl , [
CURLOPT_POST => true ,
CURLOPT_RETURNTRANSFER => true ,
CURLOPT_HTTPHEADER => [
'Authorization: Bearer ' . $openAiApiKey ,
'Content-Type: application/json'
],
CURLOPT_POSTFIELDS => $requestBody
]);
Handling the OpenAI Responses API Response - Success
After decoding the OpenAI response into a PHP associative array, Liber traverses the output collection by response type rather than assuming a fixed array position. Message items are inspected for output_text content, and the corresponding text value contains the schema-constrained semantic-search JSON. That JSON is then decoded into a PHP array and normalized into the expected postCategory and postTags response shape before domain validation and return to the calling application.
The shape of the response decoded as a PHP array appears in an Xdebug session as shown in the snippet below:
Listening to Xdebug on port 0.0.0.0:9003 ...
$openAiResponseArray [ 'output' ]
array(2)
0 = array(5)
1 = array(6)
id = "msg_0fb52ad248af026b006ad627054aa487d1a7eeb5364d490767"
type = "message"
status = "completed"
content = array(1)
0 = array(4)
type = "output_text"
annotations = array(0)
logprobs = array(0)
text = "{"postCategory":"Database Engineering","postTags":["Validation"]}"
phase = "final_answer"
role = "assistant"
And the traversal sequence and logic of the output collection is orchestrated in PHP as shown in the snippets below:
// Decode the OpenAI Responses API response.
$openAiResponse = curl_exec ( $curl );
$openAiResponseArray = json_decode ( $openAiResponse , true );
$semanticSearchResponseJson = null ;
// Iterate through AI response array; store relevant content from
// contentItem[].
foreach ( $openAiResponseArray [ 'output' ] ?? [] as $outputItem ) {
// Inspect content only for outputItems which are actual messages.
if (( $outputItem [ 'type' ] ?? null ) !== 'message' ) {
continue ;
}
// Find the text for the given content member.
foreach ( $outputItem [ 'content' ] ?? [] as $contentItem ) {
if (
( $contentItem [ 'type' ] ?? null ) === 'output_text'
&& isset ( $contentItem [ 'text' ])
) {
$semanticSearchResponseJson = $contentItem [ 'text' ];
break 2 ;
}
}
}
// Decode the schema-constrained semantic search JSON.
$semanticSearchResponse =
$semanticSearchResponseJson
? json_decode ( $semanticSearchResponseJson , true )
: null ;
// Normalize the expected response properties for application use.
$response = [
'postCategory' => $semanticSearchResponse [ 'postCategory' ] ?? null ,
'postTags' => $semanticSearchResponse [ 'postTags' ] ?? []
];
// Return the response data as name => value pairs.
return $response ;
Handling the OpenAI Responses API Response - Failure Modes
If LiberAnalytics does not receive a usable response from the Responses API, or the returned semantic-search payload cannot be parsed into the expected application structure, the request is handled as an upstream failure. Liber returns a 502 Bad Gateway response and a generalized error message to the client indicating the nature of the failure.
// Execute the cURL request; return 502 if the upstream request fails.
$openAiResponse = curl_exec ( $curl );
if ( $openAiResponse === false ) {
http_response_code ( 502 );
return [
'error' => 'Semantic search request failed.'
];
}
// Return 502 if the extracted semantic search response is unusable.
if ( ! is_array ( $semanticSearchResponse ))
{
http_response_code ( 502 );
return [
'error' => 'Invalid semantic search response.'
];
}
Validating LLM Responses Against Authoritative Domain Data
As a best practice when consuming server-side or upstream response data, whether generated by application code or returned by OpenAI's Responses API, the returned values should be validated against the application's authoritative domain before they are accepted for execution. In the snippets below, the returned post category and post tags are checked for membership in LiberAnalytics' authoritative domain data. If either contains an invalid value, LiberAnalytics returns an HTTP 502 Bad Gateway response and a generalized error message to the client application.
// Make sure the post category is in the authoritative membership domain.
if (
$response [ 'postCategory' ]
&& ! in_array ( $response [ 'postCategory' ], $postCategories , true )
) {
http_response_code ( 502 );
return [
'error' => 'Invalid post category.'
];
}
// Make sure the post tags are in the authoritative membership domain.
$invalidPostTags = array_diff ( $response [ 'postTags' ], $postTags );
if ( $invalidPostTags ) {
http_response_code ( 502 );
return [
'error' => 'Invalid post tag(s).'
];
}
Preparing LiberAnalytics’ Data and UI for Governed LLM Responses
Before LiberAnalytics could implement a call to the OpenAI Responses API, its own deterministic data pipeline had to be hardened to enforce contracts between calling and receiving application components and to allow the user interface to consume and render error messages flexibly.
- The PostgreSQL function responsible for retrieving authoritative tag-domain data previously required a TEXT[] parameter. By making that parameter optional with DEFAULT NULL and treating NULL as a signal to return all tags, LiberAnalytics could expose the complete authoritative tag set for inclusion in the semantic-search JSON Schema supplied to the OpenAI Responses API.
- A final validation step was added to the OpenAI Responses API orchestration to verify that the returned post category and post tags exist within LiberAnalytics’ authoritative domain. Even though the combination of the semantic-search JSON Schema and the instructions supplied in the API request constrains and guides the LLM toward domain-valid values, a defense-in-depth implementation prevents the model response from becoming application state merely because OpenAI returned it.
- A data-integrity issue existed in LiberAnalytics’ post-retrieval function where an inner join to the images table caused an otherwise valid post record to disappear from the result set when its corresponding image data was missing. After the missing image data was added, the post was returned as expected. This illustrated a broader point: for AI integration to be useful, the underlying data relationships and deterministic system workflows must be trustworthy enough for the model’s output to operate against.
-
The user interface also required the error renderer to accept parameterized messages and allow the
caller to specify which JavaScript element should receive the error. Before the AI integration,
invalid user input was handled by reading the input element’s data-id attribute and rendering
a generic message in the form <data-id> value <input value> is invalid. The refactored
renderer shown below preserves that default behavior while also allowing callers to supply more
contextual messages, such as failures associated with HTTP response codes 500, 502, and other
application or upstream errors.
export const renderError = ( {
errorEle = getDefaultErrorEle (),
errorMsg = null ,
errorKey ,
errorValue
} = {}) => {
if ( errorEle ) {
const renderMsg =
errorMsg ?? ` ${errorKey} value ${errorValue} is invalid.` ;
errorEle . textContent = renderMsg ;
}
}
Making LLM Responses Usable Within a Deterministic System
AI readiness was not about gaining access to a model. It was about making the surrounding application trustworthy enough that a probabilistic component could be introduced safely. Probabilistic reasoning should be introduced only after the deterministic semantics, contracts, and authority boundaries of the system are sufficiently defined.
By supplying a JSON Schema derived from LiberAnalytics’ authoritative post-category and post-tag domain, along with instructions governing how user-expressed search terms should be interpreted, the OpenAI Responses API can return structured data that LiberAnalytics’ existing deterministic pipeline can safely consume. The model is prevented from returning out-of-domain values by the schema contract, and the resulting values are independently validated against the authoritative domain before they are accepted for application use.