feat(search): append raw entity fields after view_node summary

view_node 按题两轮摘要(summarize_node)会吞掉 entities/visible_text
字段信号,Agent 站在证据节点上仍漏读实体(benchmark 错题 M1,案例
786-2、872-3、750-1)。dispatcher 侧在摘要后确定性追加 [实体]/[画面
文字] 原文区块,LLM 无法吞掉。

- TreeEnvironment.node_entity_fields:按层级取 card 实体字段原文,
  去空白、去重、分号拼接;空字段省键;未知节点抛 KeyError
- _handle_view_node Phase 2.5:摘要后、子节点概览前追加实体区块
- 附带 ruff format 修正 test_tree_environment.py 两处既有格式

算法 #11(树环境语义搜索)数据访问层扩展,不改搜索算法本身。
This commit is contained in:
2026-07-11 08:53:56 -04:00
parent 291a8108e1
commit 4e0e05210d
4 changed files with 226 additions and 2 deletions
+83
View File
@@ -444,3 +444,86 @@ class TestDispatchErrors:
context={},
)
assert "工具执行错误" in result
# ── view_node 实体追加测试(Spec-1 B)────────────────────────
def _make_entity_tree() -> TreeIndex:
"""L2 带实体字段的最小树(与 _make_test_tree 同构,仅换 card 内容)。"""
l2 = L2Node(
id="vid_L1_000_L2_000",
card=L2Card(
event_description="产品评测",
entities=["Bluetooth headset (both ears)", "reviewer"],
actions=["reviewing"],
action_subjects=["reviewer"],
visible_text=["$9.99"],
spatial_relations="",
state_changes=None,
),
time_range=(5.0, 15.0),
children=[],
)
l1 = L1Node(
id="vid_L1_000",
card=L1Card(
scene_summary="评测场景",
main_setting="室内",
key_entities=["reviewer"],
main_actions=["评测"],
topic_keywords=["数码"],
visible_text=[],
temporal_flow="线性",
),
time_range=(0.0, 30.0),
children=[l2],
)
return TreeIndex(
metadata=IndexMeta(source_path="test.mp4", modality="video"),
roots=[l1],
)
@pytest.fixture()
def entity_dispatcher(
prompts_dir: Path,
skills_registry: SkillRegistry,
) -> SearchToolDispatcher:
"""树含实体字段的 dispatcher(其余配置与 dispatcher fixture 一致)。"""
return SearchToolDispatcher(
env=TreeEnvironment(_make_entity_tree()),
tool_llm=FakeLLM(),
vlm=FakeVLM(),
ocr=FakeOCR(),
prompts_dir=prompts_dir,
skills=skills_registry,
embed_fn=_fake_embed_fn,
verify_vision=False,
anchor=False,
assemble_mode="ids",
)
class TestViewNodeEntityAppendix:
"""view_node 实体区块确定性追加测试。"""
@pytest.mark.asyncio()
async def test_view_node_appends_entity_blocks(
self, monkeypatch: pytest.MonkeyPatch, entity_dispatcher: SearchToolDispatcher
) -> None:
"""摘要后必须出现 [实体]/[画面文字] 区块(确定性追加,不经 LLM)。"""
async def _stub_summarize(*args: Any, **kwargs: Any) -> str:
return "[内容摘要] 与问题无关的摘要"
monkeypatch.setattr("app.search.tools.summarize_node", _stub_summarize)
result = await entity_dispatcher.dispatch(
"view_node",
{"node_id": "vid_L1_000_L2_000", "question": "耳机戴哪只耳?"},
context={},
)
assert "[实体]" in result
assert "Bluetooth headset (both ears)" in result
assert "[画面文字]" in result
assert "$9.99" in result