{"id":4807,"date":"2025-06-02T11:13:57","date_gmt":"2025-06-02T18:13:57","guid":{"rendered":"https:\/\/devblogs.microsoft.com\/azure-sql\/?p=4807"},"modified":"2025-06-04T11:19:53","modified_gmt":"2025-06-04T18:19:53","slug":"a-story-of-collaborating-agents-chatting-with-your-database-the-right-way","status":"publish","type":"post","link":"https:\/\/devblogs.microsoft.com\/azure-sql\/a-story-of-collaborating-agents-chatting-with-your-database-the-right-way\/","title":{"rendered":"A story of collaborating agents: chatting with your database the right way"},"content":{"rendered":"<p>Today, more and more customers are embracing AI with one clear goal in mind: they want to chat with their data. And by &#8220;data,&#8221; they mean all of it:\u00a0 structured tables, unstructured documents, images, audio, and more.<\/p>\n<p>This demand has given rise to hybrid approaches that blend semantic understanding with precise data retrieval. But here\u2019s the challenge: while embeddings and vector search play a critical role in semantic search, they\u2019re not enough on their own \u2014 especially when the question is highly specific and demands an exact answer.<\/p>\n<p>As I discussed in <a href=\"https:\/\/devblogs.microsoft.com\/azure-sql\/improve-the-r-in-rag-and-embrace-agentic-rag-in-azure-sql\/\">Improve the \u201cR\u201d in RAG and Embrace Agentic RAG in Azure SQL<\/a> article, a smarter, multi-step approach is needed.<\/p>\n<p>There\u2019s a code sample waiting for you at the end\u2014but don\u2019t scroll just yet! This article lays the foundation you\u2019ll need to really understand the strategy behind it. That way, you\u2019ll be able to apply the same pattern to your own data, not just the example. So read through it all\u2026 and no, asking an AI to summarize it won\u2019t cut it this time \ud83d\ude09<\/p>\n<h2>Two Kinds of Questions, Two Kinds of Answers<\/h2>\n<p>When users ask a question, it typically falls into one of two categories:<\/p>\n<ol>\n<li><em>Unstructured Data Search:<\/em> The question targets unstructured content (text, image, audio), and the goal is to find semantically similar results \u2014 this is where vector search and embeddings shine.<\/li>\n<li><em>Precise, Structured Query:<\/em> The question is clear-cut and requires an exact answer from structured data. Embeddings offer little help here. Instead, we need to generate and run a valid SQL query against the correct set of tables.<\/li>\n<\/ol>\n<p><strong>This post focuses on the second case, <\/strong>as it&#8217;s arguably the most complex.<\/p>\n<p>Getting a precise, correct answer from a database using natural language is where LLMs struggle the most. They&#8217;re inherently designed to provide semantically relevant responses, not necessarily factually or structurally accurate ones. That\u2019s why this problem requires a carefully crafted multi-step approach, where the LLM is guided to do what it does best while minimizing the risk of error.<\/p>\n<p>So how do you apply this to a real-world enterprise database with hundreds of tables?<\/p>\n<h2>\u201cDivide and Conquer\u201d Still Works<\/h2>\n<p>The classic strategy of &#8220;divide and conquer&#8221; is your best friend here. You can\u2019t simply dump the full schema of 100+ tables into a large language model (LLM) and expect magic. LLMs have limitations \u2014 especially when it comes to context window size and relevance filtering.<\/p>\n<p>The more irrelevant data you include, the more noise you&#8217;ll actually generate and so the less effective the model becomes. If you\u2019re asking a question about product pricing, providing the LLM with address type definitions only adds confusion. So, the problem needs to be solved with two separate steps: first, help the LLM to figure out what table are really needed, second help the LLM model to have detailed understanding of the schema of those tables, to that the best SQL query can be generate.<\/p>\n<h3>Agent 1: Create a high-level, domain expert<\/h3>\n<p>Start by giving the LLM a high-level map of your schema. Not every column, not every data type \u2014 just the purpose of each table.<\/p>\n<p>Here\u2019s an example from the AdventureWorks2022 database, that can be used to define the prompt for the first agent:<\/p>\n<pre class=\"prettyprint language-default\"><code class=\"language-default\">[Production].[ProductListPriceHistory]: Changes in the list price of a product over time.\r\n[Person].[Address]: Street address information for customers, employees, and vendors.\r\n[Sales].[SpecialOfferProduct]: Cross-reference table mapping products to special offer discounts.\r\n[Production].[ProductModel]: Product model classification.\r\n[Person].[AddressType]: Types of addresses stored in the Address table.\r\n[Person].[StateProvince]: State and province lookup table.<\/code><\/pre>\n<p>The prompt would look like the following:<\/p>\n<pre class=\"prettyprint language-default\"><code class=\"language-default\">You are an AI assistant that helps users to query the database. The tables in the database, with the related description, are:\r\n\r\n{await database.GetTablesAsync()}                    \r\n\r\nUse a professional tone when aswering and provide a summary of data instead of lists. \r\nIf users ask about topics you don't know, answer that you don't know. Today's date is {DateTime.Now:yyyy-MM-dd}. \r\nYou must answer providing a list of tables that must be used to answer the question and an explanation of that you'll be doing to answer the question.\r\nYou must use the provided tool to query the database.\r\nIf the request is complex, break it down into smaller steps and call the plugin as many time as needed. Ideally don't use most than 5 tables in the same query.            \r\n<\/code><\/pre>\n<p>With this context, you ask the LLM to identify the most relevant tables needed to answer a specific question. <strong>Think of the LLM in this phase as a knowledgeable data architect \u2014 someone who knows where the data lives, but not exactly how it\u2019s structured inside each table.<\/strong><\/p>\n<h3>Agent 2: Create a SQL expert<\/h3>\n<p>Once you have the list of candidate tables, then \u2014 and only then \u2014 you provide full schema details: column names, types, relationships, and even quirks like embedded JSON blobs in \u201cdetails\u201d columns.<\/p>\n<p>This is your opportunity to give the LLM the context it actually needs, and nothing more. If a column holds JSON, you should explain its structure and provide examples of how to extract data from it.<\/p>\n<p>Here\u2019s a sample of how you might present that information to the LLM for the <code>[Sales].[SalesTerritory]<\/code> table:<\/p>\n<pre class=\"prettyprint language-default\"><code class=\"language-default\">[TerritoryID] int -- Primary key for SalesTerritory records.\r\n[Name] Name -- Sales territory description\r\n[CountryRegionCode] nvarchar -- ISO standard country or region code. Foreign key to CountryRegion.CountryRegionCode.\r\n[Group] nvarchar -- Geographic area to which the sales territory belongs.<\/code><\/pre>\n<p>In this second agent, the prompt is the following:<\/p>\n<pre class=\"prettyprint language-default\"><code class=\"language-default\">You create T-SQL queries based on the given user request and the provided schema. Just return T-SQL query to be executed. \r\nDo not return other text or explanation. Don't use markdown or any wrappers.\r\nThe schema is provided in the format: \r\n        \r\nTable1Name: \r\nColumn1Name (Column1Type) -- Column1Description\r\nColumn2Name (Column2Type) -- Column2Description\r\n...\r\nColumnNName (ColumnNType) -- ColumnNDescription\r\n\r\nTable2Name: \r\nColumn1Name (Column1Type) -- Column1Description\r\nColumn2Name (Column2Type) -- Column2Description\r\n...\r\nColumnNName (ColumnNType) -- ColumnNDescription\r\n\r\nThe schema for the avaiable tables is the following:\r\n        \r\n{table_schemas.ToString()}\r\n        \r\nGenerate the T-SQL query based on the provided schema and the user request. The user request is in the next message.<\/code><\/pre>\n<p>This type of structured metadata, minimal yet meaningful, gives the LLM enough signal to reason about what this table is for, without overwhelming it with noise. You can then repeat the process for other selected tables, tailoring the level of detail based on what the question requires.<\/p>\n<p>By feeding the model only relevant, targeted information like this, you dramatically reduce confusion and increase the accuracy of the generated SQL query. You have now created a SQL expert that knows exactly how to query the involved tables.<\/p>\n<h3>Making the Agents Working Together<\/h3>\n<p>This is where the Agentic approach shines. Think of this system as having two expert agents, each with a specific role:<\/p>\n<ul>\n<li><strong>Agent 1 is the domain expert.<\/strong> It knows the full database layout at high-level and can identify which tables are relevant based on the user&#8217;s question.<\/li>\n<li><strong>Agent 2 is the query expert<\/strong>. It writes SQL queries using only the relevant tables and a clear goal, as instructed by Agent 1.<\/li>\n<\/ul>\n<p>Here\u2019s how a real interaction might play out:<\/p>\n<p><strong>Human:<\/strong> Hi, I want to know who is the most profitable customer.<\/p>\n<p><strong>Agent1:<\/strong> I need to query the database using <em>[Sales].[Customer], [Sales].[SalesOrderHeader], [Sales].[SalesOrderDetail]. <\/em>Asking <em>Agent2<\/em> to: &#8220;Identify the customer who has contributed the highest total profit. Sum up the profit for each customer using order amounts and subtracting related costs if available. Return the most profitable customer details including customer ID, name if available, and total profit.&#8221;<\/p>\n<p><strong>Agent2<\/strong>: Running the following query:<\/p>\n<pre class=\"prettyprint language-sql\"><code class=\"language-sql\">SELECT TOP 1\r\n  C.CustomerID,\r\n  C.AccountNumber,\r\n  SUM(SOD.LineTotal) AS TotalProfit\r\nFROM\r\n  [Sales].[Customer] AS C\r\nJOIN\r\n  [Sales].[SalesOrderHeader] AS SOH ON C.CustomerID = SOH.CustomerID\r\nJOIN\r\n  [Sales].[SalesOrderDetail] AS SOD ON SOH.SalesOrderID = SOD.SalesOrderID\r\nGROUP BY\r\n  C.CustomerID, C.AccountNumber\r\nORDER BY\r\n  TotalProfit DESC;<\/code><\/pre>\n<p><strong>Answer<\/strong>: The most profitable customer is the one associated with Customer ID 29818 and Account Number AW00029818. This customer has contributed a total profit of approximately $877,107.19 to the business. If you need further details about this customer, let me know!<\/p>\n<p><strong>Human<\/strong>: Can you do a breakdown per year and then also return to me the name and some details about the customer?<\/p>\n<p><strong>Agent1<\/strong>: Use the tables: <em>[Sales].[Customer], [Sales].[SalesOrderHeader], [Sales].[SalesOrderDetail]<\/em>. Asking <em>Agent2<\/em> to: &#8220;Break down the total profit data of Customer ID 29818 by year and provide yearly profit details for this customer.&#8221;<\/p>\n<p><strong>Agent2:<\/strong> <em>&lt;Generates and runs the SQL query to return yearly profit breakdown&gt;<\/em><\/p>\n<p><strong>Agent1<\/strong>: <em>Agent2<\/em>, now use <em>[Sales].[Customer], [Person].[Person]<\/em> to: &#8220;Provide the name and additional details of the customer with Customer ID 29818, including contact or relevant information.&#8221;<\/p>\n<p><strong>Agent2:<\/strong> <em>&lt;Generates and runs the SQL to return customer name, email, etc.&gt;<\/em><\/p>\n<p><strong>Answer:<\/strong> <em>&lt;final answer&gt;<\/em><\/p>\n<p>This flow mirrors real-world collaboration: schema design and data retrieval are distinct tasks, and each expert focuses on what they do best. The LLMs emulate that by acting as coordinated, specialized agents.<\/p>\n<h2>What\u2019s Next: From Queries to Conversations<\/h2>\n<p>The approach we\u2019ve outlined, combining schema-aware reasoning with agentic collaboration, is a powerful way to enable natural language access to structured data. But in most real-world scenarios, structured data is only part of the story.<\/p>\n<p>To truly \u201cchat with your data,\u201d you need to handle both structured and unstructured sources \u2014 and that\u2019s where <strong>RAG pattern<\/strong> comes in.<\/p>\n<p>In practice, this means your system needs to make an early decision: <em>Is this question best answered with a SQL query, or does it require semantic search over documents, images, or other unstructured content?<\/em><\/p>\n<p>This decision is the first and most critical task for the LLM. As discussed in <a href=\"https:\/\/devblogs.microsoft.com\/azure-sql\/improve-the-r-in-rag-and-embrace-agentic-rag-in-azure-sql\/\">Improve the \u201cR\u201d in RAG and Embrace Agentic RAG in Azure SQL<\/a>, the best systems combine both approaches:<\/p>\n<ul>\n<li>Use <em>Agentic SQL generation<\/em>\u00a0when the question demands precision and structured data (e.g., \u201cWhat was our top-selling product in Q4 2023?\u201d).<\/li>\n<li>Use <em>RAG Pattern<\/em> when the question is exploratory or based on unstructured content (e.g., \u201cWhat are customers saying about our new product line?\u201d).<\/li>\n<li>And in some cases, <em>combine both<\/em>. For example, retrieving a list of customers from SQL, then enriching it with sentiment analysis from support tickets or reviews.<\/li>\n<\/ul>\n<p>This hybrid model ensures that each question is routed to the right tool for the job, maximizing both accuracy and relevance.<\/p>\n<h2>Final Word<\/h2>\n<p>Conversational access to enterprise data isn\u2019t just about writing better SQL or retrieving better documents. It\u2019s about orchestrating the right tools in the right order. That\u2019s where agentic patterns shine. By combining classic RAG with structured query generation, and by guiding LLMs through a multi-step reasoning process, you can build systems that are not only intelligent \u2014 but also trustworthy, scalable, and enterprise-ready.<\/p>\n<p>Let\u2019s make data talk \u2014 no matter what form it takes.<\/p>\n<h2>Try it yourself<\/h2>\n<p>The source code, as usual, is available on GitHub, here: <a href=\"https:\/\/github.com\/Azure-Samples\/azure-sql-nl2sql\">https:\/\/github.com\/Azure-Samples\/azure-sql-nl2sql.<\/a><\/p>\n<p>The sample is on purpose super simple, to make it easier for everyone to understand what&#8217;s going on. An improvement would be to use structured output to return the generated SQL query to make it easier to execute it. Execution of generated SQL of course, is done without any security check which is obviously not a best practice: in a real world deployment, the generate SQL *must* be sanitized and checked before executing it. I would also strongly recommend making sure you have all the proper security configuration done in the database, especially including <a href=\"https:\/\/learn.microsoft.com\/en-us\/sql\/relational-databases\/security\/row-level-security?view=sql-server-ver17\">Row-Level Security<\/a>, to avoid any possible prompt injection attack that could cause data leaks or worse. Having said that, have fun with the code, it is going to make your day, I promise!<\/p>\n","protected":false},"excerpt":{"rendered":"<p>Today, more and more customers are embracing AI with one clear goal in mind: they want to chat with their data. And by &#8220;data,&#8221; they mean all of it:\u00a0 structured tables, unstructured documents, images, audio, and more. This demand has given rise to hybrid approaches that blend semantic understanding with precise data retrieval. But here\u2019s [&hellip;]<\/p>\n","protected":false},"author":24720,"featured_media":4811,"comment_status":"open","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"_acf_changed":false,"footnotes":""},"categories":[601,1],"tags":[673,590,674,613,627,486],"class_list":["post-4807","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-ai","category-azure-sql","tag-agents","tag-ai","tag-natural-language","tag-nl2sql","tag-rag","tag-sql"],"acf":[],"blog_post_summary":"<p>Today, more and more customers are embracing AI with one clear goal in mind: they want to chat with their data. And by &#8220;data,&#8221; they mean all of it:\u00a0 structured tables, unstructured documents, images, audio, and more. This demand has given rise to hybrid approaches that blend semantic understanding with precise data retrieval. But here\u2019s [&hellip;]<\/p>\n","_links":{"self":[{"href":"https:\/\/devblogs.microsoft.com\/azure-sql\/wp-json\/wp\/v2\/posts\/4807","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/devblogs.microsoft.com\/azure-sql\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/devblogs.microsoft.com\/azure-sql\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/azure-sql\/wp-json\/wp\/v2\/users\/24720"}],"replies":[{"embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/azure-sql\/wp-json\/wp\/v2\/comments?post=4807"}],"version-history":[{"count":0,"href":"https:\/\/devblogs.microsoft.com\/azure-sql\/wp-json\/wp\/v2\/posts\/4807\/revisions"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/azure-sql\/wp-json\/wp\/v2\/media\/4811"}],"wp:attachment":[{"href":"https:\/\/devblogs.microsoft.com\/azure-sql\/wp-json\/wp\/v2\/media?parent=4807"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/azure-sql\/wp-json\/wp\/v2\/categories?post=4807"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/azure-sql\/wp-json\/wp\/v2\/tags?post=4807"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}