尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

Azure OpenAI Agent调用AI Search只出JSON不生成答案?根因与修复

Azure OpenAI Agent调用AI Search只出JSON不生成答案?根因与修复 最近帮朋友调一个 Azure 上的 Agent 项目遇到一个特别典型的问题我们在 Azure OpenAI 里创建了一个 Agent挂上了 Azure AI Search 作为检索工具期望它回答用户问题时能够先搜资料、再组织语言给出答案。结果实测下来Agent 确实“干活”了但只干了一半——它返回了一堆 JSON 引用像是从搜索引擎原样抄过来的结果片段而不是一句完整的人话。问题一出很多人第一反应是“模型坏了”但其实这是 Agent 工作流里一个非常经典的工具调用中断问题。这篇文章就把这个“只出 JSON 不出答案”的问题彻底拆开讲一遍包括根因、定位方法、修复方案以及我在实际调试中踩过的坑和建议。1. 问题复现Agent 返回的不是答案是检索日志1.1 故障现象先看一个最小复现场景。我在 Azure OpenAI Studio 的 Assistants API 里创建了一个 Agent工具列表中挂载了 Azure AI Search 检索工具。用户提问是“我们上季度的销售总额是多少”这个问题的预期答案应该是根据检索到的文档片段整理出一段话“上季度销售总额为 2350 万元同比增长 12%……”。但实际返回的是类似下面这样的内容[ { search.score: 0.032156, content: 2024年四季度销售总额为2350万元同比增加12%。, source: sales_summary_2024.md, title: Q4 Sales Summary }, { search.score: 0.028941, content: 本季度华北区贡献了主要增长主要原因是新客群拓展。, source: sales_analysis.md } ]如果是在 API 层面调试你会发现delta或者最终消息里的content就是这段 JSON 字符串甚至有时候会把search.score这种原始字段也带出来。用户当然不满意因为问的是“多少”得到的是一个仿佛没解析完的数据结构。1.2 这个 JSON 引用到底是什么这段 JSON 实际上就是 Azure AI Search 返回的原始检索结果。Azure AI Search 的 SDK 或者 REST API 查询时返回的search_results就是一组文档对象包含命中分数search.score、检索命中的字段content、source等以及默认的元数据。换句话说Agent 确实调用了检索工具并且拿到了搜索结果。问题在于这整个流程并没有走完。在标准的“检索 生成”架构下JSON 引用只是中间产物它应该作为大模型的上下文输入用来生成最终答案。现在它却被当成了最终输出。这里的“Agent Retrieval”指的就是 Agent 执行检索工具的这个动作而“生成答案”是后续的第二步动作。如果第二步没有执行你看到的就是引用而不是答案。2. 根因分析为什么引用会替代答案2.1 Agent 工具调用的完整生命周期要理解这个现象先得把 Agent 的调用逻辑理顺。一个标准 Agent 的执行过程不是“用户问一句模型回一句”这么简单它通常包含如下几个阶段用户提交问题。LLM 根据指令判断如果要得到答案需要检索外部数据。LLM 输出一个结构化指令告诉执行层“调用搜索工具”并带上搜索参数。执行层比如 OpenAI 的 Assistants API 或自定义的 function calling 循环真正去调 Azure AI Search拿到 JSON 格式的检索结果。这个 JSON 结果会被回传给 LLM 作为新消息。LLM 依据这个 JSON 上下文生成面向用户的自然语言答案。最终答案是第 6 步的结果而不是第 4 步的原始 JSON。关键就在第 4 步和第 6 步之间。AI Search 工具只是把文档片段拉回来它不会自动“说话”。如果你看到输出是 JSON那基本可以判断为流程在第 4 步之后断了。2.2 最常见的中断原因结合我实际排查过的项目中断通常由下面三种原因导致。第一种是“拿了工具输出就直接返回”。也就是在自定义 function calling 的代码里你写了一个循环模型说“我要调用 search”然后你执行搜索然后把search_result直接作为最终应答返回给前端。这个逻辑看起来“完成了工具调用”但实际上跳过了“让模型消化工具结果”这一步。你等于拿了一个中间产物冒充最终产物。第二种是“用错了 API 层级”。很多人以为把 Azure AI Search SDK 接进来就是 Agent实际上只是调用了 SearchClient 的search()方法拿到 JSON。这一步只完成了检索后续的生成部分完全没接入。这个问题的命名很符合你用了一个检索 API却期望它有 Agent 的生成能力。第三种是“Agent 指令不明确”。如果你用的是 Assistants API 的检索工具模型本可以将工具结果作为上下文然后生成回答。但如果系统提示词写得不够明确比如没有要求“基于检索结果给出简洁自然语言答案”模型可能就会把工具返回结果原样呈现出来特别是某些模型的指令遵循能力不够强的时候这种错误尤其容易发生。3. 定位问题确保你找到卡在哪一步3.1 查看消息记录中的角色与工具痕迹第一步把 Agent 会话里的消息列表完整打印出来看每个消息的role和内容。如果使用 Azure OpenAI Assistants API有一个接口能拉取 Thread 里的 message 列表。我习惯写一个调试脚本from openai import AzureOpenAI client AzureOpenAI( azure_endpointhttps://your-resource.openai.azure.com/, api_keyyour-key, api_version2024-05-01-preview ) run_thread_id thread_xxx messages client.beta.threads.messages.list(thread_idrun_thread_id) for msg in messages.data: print(frole: {msg.role}) for content in msg.content: text content.text.value if content.type text else content.type print(text) print(---)在正常流程里你会看到类似这样的消息顺序user上季度的销售总额是多少assistant调用search_sales_data工具参数为{query: 上季度销售总额}这里在消息内容里会显示为 function call 或者工具调用标记。tool返回了那段 JSON 引用。assistant最终自然语言答案。如果你看到最后一条assistant消息的内容就是那段 JSON或者消息序列直接中断在tool层就说明模型根本没有在工具结果之后继续生成。3.2 检查你用的是 Agent 还是原生搜索这一步很多人会忽略。你需要确认当前调用链路的实际结构。如果在代码里直接用了azure-search-documents的SearchClient.search()方法那你走的根本不是 Agent 链路而是普通检索。该方法的返回值就是SearchResult的迭代器序列化后自然是 JSON。from azure.search.documents import SearchClient from azure.search.documents.models import SearchOptions results search_client.search( search_text上季度销售总额, top3 ) for result in results: print(result[search.score]) print(result[content])这就是纯检索没有任何模型生成环节。此时你得到 JSON 是完全正常的。想象一下如果你把这段代码结果直接返回给用户你看到的当然只有 JSON。所以排查第一步就是问自己我用的是 Agent 服务还是仅仅用了搜索客户端如果是后者那问题不是“Agent 不生成答案”而是“你根本没有 Agent”。4. 解决方案让 Agent 把 JSON 变成人话4.1 方案一手动完成 tool call 后的二次生成如果你在做自定义 Agent比较简单粗暴的方式是手动模拟完整的 function calling 循环。流程就是调用模型传入用户消息和工具定义。模型返回工具调用指令。执行搜索得到 JSON 结果。将 JSON 结果附加为一条tool角色消息再调用一次模型。拿到最终自然语言输出。下面是一个精简但完整可运行的思路用的是 OpenAI Python SDK 里的 chat completionsAzure 同样适用from openai import AzureOpenAI import json client AzureOpenAI( azure_endpointhttps://your-resource.openai.azure.com/, api_keyyour-key, api_version2024-06-01 ) def search_documents(query): # 这里使用你的 Azure AI Search 逻辑返回 json 字符串 return json.dumps(search_json_list(query), ensure_asciiFalse) messages [ {role: user, content: 上季度的销售总额是多少} ] tools [ { type: function, function: { name: search_documents, description: 搜索企业内部文档或数据报告, parameters: { type: object, properties: { query: {type: string, description: 用户的搜索意图} }, required: [query] } } } ] # 第一次调用让模型决定是否调用搜索 response client.chat.completions.create( modelgpt-4o, messagesmessages, toolstools, tool_choiceauto ) # 检查是否是工具调用 if response.choices[0].message.tool_calls: tool_call response.choices[0].message.tool_calls[0] args json.loads(tool_call.function.arguments) query args[query] search_result search_documents(query) # 将工具结果追加到消息 messages.append(response.choices[0].message) messages.append({ role: tool, tool_call_id: tool_call.id, content: search_result }) # 第二次调用让模型基于检索结果生成最终答案 final_response client.chat.completions.create( modelgpt-4o, messagesmessages, ) print(final_response.choices[0].message.content) else: # 模型没有调用工具直接生成 print(response.choices[0].message.content)关键在于第二次create。如果缺失你拿到的就是第一次响应的tool_calls而很多人的朋友就是在这一步随手把search_result打印给前端了。4.2 方案二用高层 RAG 编排链一步到位如果不打算手动写循环又不想依赖 Assistants 的隐式行为直接用 LangChain 的 RetrievalQA 链反而是更稳的路径。LangChain 有现成的AzureSearch向量存储类也有AzureChatOpenAI模型封装可以这些组件组装成一条链。from langchain.vectorstores.azure_search import AzureSearch from langchain.chat_models import AzureChatOpenAI from langchain.chains import RetrievalQA from langchain.embeddings import AzureOpenAIEmbeddings # 初始化检索器 embedder AzureOpenAIEmbeddings( azure_deploymenttext-embedding-3-small, openai_api_version2023-05-15 ) vector_store AzureSearch( azure_search_endpointhttps://your-search.search.windows.net, azure_search_keyyour-search-key, index_nameyour-index, embedding_functionembedder.embed_query, ) retriever vector_store.as_retriever(search_typesimilarity, search_kwargs{k: 3}) # 初始化 LLM llm AzureChatOpenAI( azure_deploymentyour-gpt4o-deployment, openai_api_version2024-02-01, api_keyyour-openai-key, temperature0.2 ) qa_chain RetrievalQA.from_chain_type( llmllm, chain_typestuff, retrieverretriever, return_source_documentsTrue ) result qa_chain(上季度的销售总额是多少) print(result[result])这段代码的好处是 LangChain 内部自动完成了“检索 - 拼接上下文 - 大模型生成”的过程最终result[result]就是自然语言答案而source_documents里的 JSON 引用则被保留在后台适合做来源标注。4.3 方案三在 Agent 指令里锁死生成要求如果你用 Assistant API 自带的检索工具除了确保没有改错调用方式之外还应该用“指令补丁”来避免模型偷懒输出 JSON。比如在instructions字段里写清楚当你使用检索工具获得引用内容后需要基于引用内容以自然语言回答用户问题。 禁止直接输出原始检索结果、JSON 数组、Markdown 表格或字段堆砌。 回答应当简洁引用来源可放在括号后面。指令看似简单但效果很直接。很多模型之所以返回 JSON就是因为你没有告诉它“不可以直接引用”。模型以为用户想看的就是“工具输出的原始格式”。一旦在指令中明确“必须生成答案”模型就会老老实实走生成流程。另一个小细节是部分模型在处理工具结果时会把 JSON 直接放入回答区域可能是因为工具结果的 token 没有被很好地标记为“上下文”。用 Assistants API 时工具结果天然是tool角色理论上不会混入答案。但如果你调用的是 chat completions 并手动塞入了带role: tool的消息注意确保 SDK 版本支持该角色不支持的话会引发解析错误。5. 检索参数与后处理让你的引用更“值得吃”5.1 调优 Azure AI Search 的返回参数不要让所有 JSON 引用都变成模型输入。模型是有上下文窗口限制的而且冗余内容会稀释关键信息。通常我会在搜索调用时重点设置三个参数top或$top只取前 3-5 条结果。取太多反而会把低分的噪声也丢给模型。select只返回模型中真正会用到的字段比如content、title。不要返回odata.etag、空 array 等无用信息。query_type如果索引配置了语义排序建议设置query_typesimple转成语义查询或者使用 semantic rerank 功能来提升命中质量。语义排序的search.score可能会变得更好解释模型从高分结果里提取内容也更容易。一个实用配置示例from azure.search.documents.models import SearchOptions search_options SearchOptions( top3, select[content, title], query_typesemantic, semantic_configuration_namemy-semantic-config, ) results search_client.search( search_textquery, search_optionssearch_options )这样得到的 JSON 会小很多每一条基本就是title 简化后的content模型处理起来压力小得多。5.2 生成的上下文压缩技巧即便你做了select有些文档的content字段也可能很长。这时候还要做一层上下文裁剪。我习惯写一个简单的封顶函数截断到约 800 到 1200 字符并根据搜索分数排序把高分文档放在最前面。这个动作能明显提升答案的召回准确率因为大模型会更注意前面的上下文。def summarize_results(results, max_chars1200): snippets [] total_len 0 for rank, r in enumerate(results): text r.get(content, ).replace(\n, ).strip() if total_len len(text) max_chars: text text[: max_chars - total_len] snippets.append(fdoc id{r.get(title, rank)} {text} /doc) total_len len(text) if total_len max_chars: break return \n.join(snippets)然后用这个压缩后的字符串作为工具返回内容模型既不会因为上下文过长而截断也不会被无关字段干扰。6. 常见问题排查速查表下面是我整理的一些故障表现、可能原因和对应的解决方法方便大家收藏备用。故障表现可能原因解决思路Agent 返回数组形式的 JSON且带search.score直接调用了 Azure Search SDK未接入 LLM 生成用 LangChain / Prompt Flow 编排检索生成或手动实现 function calling 二次生成Agent 执行了工具调用但最终消息是工具结果工具调用后没有将tool消息再次交给模型补上第二次 LLM 调用或使用 Assistants API 的隐式循环Assistant API 返回 JSON但内容没有自然语言instructions没写清楚模型以为用户需要原始引用在指令中明确“禁止直接输出检索结果只输出基于引用的答案”偶尔返回 JSON偶尔返回正常答案工具返回值恰好被模型当成了直接回复内容降低温度对工具结果做摘要后再传给模型增加强制生成提示搜索出的文档不相关导致答案乱答索引字段/content 映射不正确或搜索语义匹配差检查索引的searchable_fields、语义配置调整top和分数过滤阈值返回的 JSON 有parse error或结构损坏tool角色的消息在 API 中不受支持或内容没被 double string 包裹确认 SDK 版本支持tool角色用json.dumps对结果做一次安全序列化最后再分享一个我自己很受益的小技巧不要急着让 Agent 直接端到端地输出答案。可以先让模型判定“是否需要检索”检索之后把检索摘要单独作为一轮消息再送一次。这种“手动两段式”看似多一次调用但稳定性和可观测性都会好很多。在多个真实项目里这种方案几乎没有再遇到“JSON 当答案”的问题。如果你也在 Azure 上折腾 Agent AI Search不妨先按这个思路自查一遍。
返回列表