AI applications are becoming increasingly useful for answering questions that require current information. With web search integration, a language model can retrieve information from external sources and use it to generate a response.
However, there is an important design challenge that developers should not overlook: retrieved information and AI-generated explanations are not the same thing.
A search result contains information retrieved from a source. An AI-generated answer is the model's interpretation of that information. If an application combines both into one block of text without preserving their relationship, users may find it difficult to understand which information came from a source and which parts were generated by the model.
This becomes particularly important in applications that provide technical documentation, financial information, product comparisons, research summaries, or security recommendations.
Amazon Bedrock Web Search returns source citation annotations with generated responses. Developers can use these annotations to preserve source information and display it separately from the generated answer.
In this article, we will explore how to design this separation in a C# application using ASP.NET Core and Amazon Bedrock.
Why Search Results and AI-Generated Answers Should Be Separate
Consider an application that answers questions about the latest .NET release.
A user asks:
"What are the important changes in the latest .NET release?"
The application performs a web search and retrieves information from official documentation and technical articles.
The model then generates a response explaining the changes in simple language.
The response may contain several different types of information:
- Information directly supported by retrieved sources.
- Explanations generated by the model.
- Comparisons based on multiple sources.
- Recommendations created by the model.
- Additional context based on the model's existing knowledge.
If the application displays everything as one paragraph, users cannot easily distinguish these categories.
For example, suppose the model generates this response:
"The latest release introduces improvements to application performance. Developers should also consider updating their existing packages before upgrading production services."
The first statement may be supported by an official release document. The second may be a general recommendation generated by the model.
Both statements may be useful, but they do not necessarily have the same source.
A better application design preserves the generated answer and its source citations as separate pieces of information.
Understand How Amazon Bedrock Returns Citations
Amazon Bedrock Web Search integrates with the Responses API.
When a supported model uses Web Search, the response contains generated text and URL citation annotations.
The annotations provide information such as:
- Source title.
- Source URL.
- Starting position of the cited text.
- Ending position of the cited text.
A simplified response structure looks like this:
{
"output": [
{
"type": "message",
"content": [
{
"type": "output_text",
"text": "The latest release introduces several improvements.",
"annotations": [
{
"type": "url_citation",
"title": "Official Release Documentation",
"url": "https://example.com/release",
"start_index": 0,
"end_index": 65
}
]
}
]
}
]
}The example is illustrative. The actual response can contain multiple output items, content blocks, and annotations.
The important detail is that citations are attached to the generated text rather than returned as an unrelated list of search results.
This allows the application to understand which part of the generated response is associated with a particular source.
It is also important to understand what a citation does not tell you. A citation can be based on a Search observation even when Fetch did not retrieve the complete page. Therefore, the presence of a citation does not prove that the full source document was fetched or independently verified.
Design a Separate Response Model
Before writing the parsing logic, define how the application should represent the answer.
Instead of returning a single string from the AI service, create a response model that contains the generated answer and a separate collection of sources.
For example:
public class AIAnswerResponse
{
public string Answer { get; set; } = string.Empty;
public List<SourceReference> Sources { get; set; } = new();
}
public class SourceReference
{
public string Title { get; set; } = string.Empty;
public string Url { get; set; } = string.Empty;
public int StartIndex { get; set; }
public int EndIndex { get; set; }
}This structure gives the application two responsibilities:
- Answer contains the text generated by the model.
- Sources contains the references returned through citation annotations.
Keeping these properties separate makes the API easier to consume from web, mobile, and desktop clients.
It also prevents the frontend from having to interpret the original Amazon Bedrock response format.
Extract Generated Text and Citations in C#
Let's create a service that reads the Responses API output and extracts the generated text and source annotations.
The following example assumes that the application has already received a successful response from Amazon Bedrock.
using System.Text.Json;
public static class BedrockResponseParser
{
public static AIAnswerResponse Parse(string responseBody)
{
using var document = JsonDocument.Parse(responseBody);
var result = new AIAnswerResponse();
var root = document.RootElement;
if (!root.TryGetProperty("output", out var output))
{
return result;
}
foreach (var item in output.EnumerateArray())
{
if (!item.TryGetProperty("type", out var itemType) ||
itemType.GetString() != "message")
{
continue;
}
if (!item.TryGetProperty("content", out var content))
{
continue;
}
foreach (var block in content.EnumerateArray())
{
if (!block.TryGetProperty("type", out var blockType) ||
blockType.GetString() != "output_text")
{
continue;
}
if (block.TryGetProperty("text", out var text))
{
result.Answer += text.GetString();
}
if (!block.TryGetProperty(
"annotations", out var annotations))
{
continue;
}
foreach (var annotation in annotations.EnumerateArray())
{
if (!annotation.TryGetProperty(
"type", out var annotationType) ||
annotationType.GetString() != "url_citation")
{
continue;
}
var source = new SourceReference
{
Title = annotation.TryGetProperty(
"title", out var title)
? title.GetString() ?? string.Empty
: string.Empty,
Url = annotation.TryGetProperty(
"url", out var url)
? url.GetString() ?? string.Empty
: string.Empty,
StartIndex = annotation.TryGetProperty(
"start_index", out var start)
? start.GetInt32()
: 0,
EndIndex = annotation.TryGetProperty(
"end_index", out var end)
? end.GetInt32()
: 0
};
result.Sources.Add(source);
}
}
}
return result;
}
}
Join the conversation! Your thoughts help the community grow.