[Bug]: try_parse_json_object turns backslash escapes into spaces when valid JSON comes in a code fence or after a sentence

未关闭 适合新手
#2,626 1 条评论 0 个 reaction 已指派 0 人 在 GitHub 查看

维护者通常 5 天内回复

还没有人认领这个 Issue。

评估

难度
2/5
预计耗时
1-3 小时
新手友好度
74/100
Issue 类型
缺陷
描述清晰度
描述清楚
活跃度
活跃
技术栈
python
领域
ai

调研方向

从 packages/graphrag/graphrag/query/llm/text_utils.py 中的 try_parse_json_object 开始,在其中从第一个 { 到最后一个 } 的截取内容会先经过 .replace 的反斜杠清理,然后才进行任何有效性检查。运行 issue 中的复现脚本,确认 line1\nline2 会返回 line1 nline2;并检查全局搜索的 search.py 中的 GlobalSearch._parse_search_response 是否存在同样的问题。完成标准是:有效的截取 JSON 保留其转义字符;无效输入仍然经过旧的清理逻辑和 json_repair;回归测试覆盖带代码围栏和带前缀的响应。

由索引模型根据 Issue 内容生成。

描述

Do you need to file an issue?
  • I have searched the existing issues and this bug is not already filed.
  • My model is hosted on OpenAI or Azure. If not, please look at the "model providers" issue and don't file a new one here.
  • I believe this is a legitimate bug, not just a question. If this is a question, please use the Discussions area.
Describe the bug

try_parse_json_object in packages/graphrag/graphrag/query/llm/text_utils.py first calls json.loads on the whole response. If the model wraps valid JSON in a markdown fence or puts a sentence before it, that call fails. The function then cuts out the text from the first { to the last } and runs a cleanup chain on it. One step of that chain is .replace("\\", " "). It runs before anything checks whether the cut-out text is already valid JSON.

So every escape inside a string value is broken, even though the cut-out text was valid:

  • \n becomes a space and the letter n. line1\nline2 comes back as line1 nline2.
  • \" becomes a space and a quote. He said \"hi\" today comes back as He said "hi " today.
  • \\ becomes two spaces. C:\\data\\x.txt comes back as C: data x.txt.
  • \t, \r and \u00e9 are broken the same way.

The same JSON without the fence parses correctly, because the first json.loads succeeds.

This function parses model output in three query paths:

  • the global search map step, GlobalSearch._parse_search_response (the description text of each key point)
  • rate_relevancy in dynamic community selection
  • DRIFT, DriftAction.search (the response text and follow_up_queries)

Map and DRIFT answers are markdown, so they often contain \n. A fenced map response is a real case: #1173 shows gemma2 on ollama returning the map response inside a ```json fence. Also, #2625 shows that response_format_json_object=True does not reach the model on main, and it reports that Qwen and Mistral on vLLM sometimes answer with prose around the JSON. I did not find a trace from OpenAI or Azure, so this mostly affects local and other non-JSON-mode providers.

Back in #801 a maintainer review asked for a json.loads check first, so valid JSON would skip the string cleanup (done in #834). That check only covers the whole response. It does not cover the object cut out of a fenced or prefixed response.

Steps to reproduce

No model is needed. With graphrag installed from main:

import json

from graphrag.query.llm.text_utils import try_parse_json_object
from graphrag.query.structured_search.global_search.search import GlobalSearch

BS = "\\"
raw = "```json\n" + '{"d": "line1' + BS + 'nline2"}' + "\n```"
expected = json.loads(raw[raw.index("{") : raw.rindex("}") + 1])
_, got = try_parse_json_object(raw, verbose=False)
print("expected:", repr(expected))
print("got:     ", repr(got))

map_raw = (
    "```json\n"
    + '{"points": [{"description": "first line' + BS + 'nsecond line, said '
    + BS + '"ok' + BS + '"", "score": 80}]}'
    + "\n```"
)
print(GlobalSearch._parse_search_response(None, map_raw))
Expected Behavior

When the cut-out object is already valid JSON, it should be returned as parsed. The cleanup and json_repair should only run when it is not valid.

Expected output:

expected: {'d': 'line1\nline2'}
got:      {'d': 'line1\nline2'}
[{'answer': 'first line\nsecond line, said "ok"', 'score': 80}]
GraphRAG Config Used
# Not needed. The reproduction calls the parser directly and uses no settings file.
Logs and screenshots

Output on main:

expected: {'d': 'line1\nline2'}
got:      {'d': 'line1 nline2'}
[{'answer': 'first line nsecond line, said  "ok "', 'score': 80}]
Additional Information
  • GraphRAG Version: 3.3.0 (main at 34b2b67)
  • Operating System: Windows 11
  • Python Version: 3.13
  • Related Issues: #1173 (fenced map response from gemma2 on ollama, fixed the regex then). #2625 (JSON mode is not sent to the model). Point 2 of #2625 also shows the backslash cleanup splitting single-quoted follow-up queries at \'. That input is not valid JSON, so the fix below does not change it.
  • I have a small fix with a regression test. After the cut-out step it tries json.loads and returns the result when it is a non-empty dict. Everything else still goes through the old cleanup path. I will open a PR that references this issue.

Validated with Claude Opus 5.5.

主要语言
Python
星标
36.6k
派生
3.9k
平均合并
4 天 11 小时
30 天内合并 PR
1

环境准备

从这里开始

  1. 先读完整个 Issue,再读项目的贡献指南。
  2. 在 Issue 下留言说明你要接手 —— 这能避免两个人做同样的事。
  3. Fork 仓库,在一个分支上完成修改。
  4. 提交 Pull Request,并在描述里引用这个 Issue 编号。

microsoft/graphrag 的其他 Issue

查看 microsoft/graphrag 的全部 Issue

相似的 Issue

更多 Python Issue

把新 issue 发到你的邮箱

精选适合新手参与的 GitHub issue 摘要。