fix(agent): keep response policy in managed prompt

This commit is contained in:
2026-07-16 17:16:51 +08:00
parent 1b5fcc4d52
commit 921b56cfb9
2 changed files with 3 additions and 64 deletions

View File

@@ -1,6 +1,5 @@
from __future__ import annotations
import re
from dataclasses import dataclass
from sqlalchemy import select
@@ -128,8 +127,6 @@ class PromptService:
if not context:
context = "本轮没有可靠的正式知识章节。对于一般常识可以谨慎回答;涉及课程、老师观点或公司业务时必须说明依据不足,不得编造。"
completeness_constraint = cls._list_completeness_constraint(question, chunks)
messages: list[dict[str, str]] = [
{
"role": "system",
@@ -157,47 +154,11 @@ class PromptService:
messages.append({
"role": "system",
"content": (
"[本轮可靠知识上下文]\n"
"以下章节均来自当前正式开放、已发布的知识库,可以作为本轮回答的可靠依据。"
"如果其中已经明确包含用户询问的课程、作业、功课或练习,不得再以‘没有资料’、"
"‘无法确认’或要求用户补充课程名称来回避回答;应直接根据章节标题和正文归纳。"
"用户问‘某课程的作业是什么’时,含义是询问该课程包含哪些作业或练习,"
"不要求知识库中必须存在一个与‘某课程作业’完全同名的章节。"
"回答作业清单类问题时,应直接给出作业名称,并概括每项作业的用途或核心方向;"
"正文已经提供基本步骤时可以简要说明,不要把本轮已经取到的内容留到下一轮再问。"
"章节标题可以作为作业或练习名称,标题下的子章节是对应的正式说明。"
f"{completeness_constraint}\n\n"
f"{context}"
),
"content": f"[本轮可靠知识上下文]\n{context}",
})
messages.append({"role": "user", "content": question.strip()})
return messages
@staticmethod
def _list_completeness_constraint(question: str, chunks: list[RetrievedChunk]) -> str:
is_practice_list = (
any(marker in question for marker in ("作业", "功课", "练习"))
and any(marker in question for marker in ("有哪些", "是什么", "包括什么", "都有什么", "列出", "汇总", "总结"))
)
numbered = [
chunk for chunk in chunks
if re.search(r"(?:练习|作业|功课)\s*[一二三四五六七八九十百\d]+", chunk.title)
]
if not is_practice_list or not numbered:
return ""
titles = "\n".join(
f"{index}. {chunk.knowledge_name} / {chunk.title}"
for index, chunk in enumerate(numbered, start=1)
)
return (
"\n\n[作业清单完整性约束]\n"
f"本轮共召回 {len(numbered)} 个正式编号练习。回答必须覆盖下面全部 {len(numbered)} 项,"
"不得只挑选部分项目,不得合并成少数概念,不得新增清单外的作业。"
"应按知识库分组,逐项保留正式练习名称,并根据对应正文简要概括;即使回答较长也必须列完。\n"
f"必须覆盖的标题清单:\n{titles}"
)
@staticmethod
def render_messages(messages: list[dict[str, str]]) -> str:
role_labels = {"system": "系统", "user": "用户", "assistant": "大本营答疑助手"}
@@ -208,6 +169,8 @@ class PromptService:
@staticmethod
def _clean_history_content(content: str) -> str:
import re
return re.sub(r"<think[\s\S]*?</think\s*>", "", content, flags=re.IGNORECASE).strip()
@classmethod

View File

@@ -21,7 +21,6 @@ from app.models.knowledge import (
)
from app.models.chat import ChatMessage
from app.services.knowledge_agent_service import Candidate, KnowledgeAgentService
from app.services.rag_service import PromptService, RetrievedChunk
def _database() -> Session:
@@ -146,9 +145,6 @@ def test_course_question_searches_chunk_and_reads_parent_section():
knowledge_context = next(
message["content"] for message in result.messages if message["content"].startswith("[本轮可靠知识上下文]")
)
assert "不得再以‘没有资料’" in knowledge_context
assert "不要求知识库中必须存在一个" in knowledge_context
assert "不要把本轮已经取到的内容留到下一轮再问" in knowledge_context
assert "来源知识库:亲子课程" in knowledge_context
@@ -228,26 +224,6 @@ def test_homework_overview_keeps_all_numbered_practices_before_selection():
assert all(item.chunk.title.startswith("练习") for item in selected)
def test_homework_overview_prompt_requires_every_recalled_title():
chunks = [
RetrievedChunk(
knowledge_id=1 if index <= 6 else 2,
knowledge_name="《原生·上》" if index <= 6 else "《原生·下》",
title=f"练习{index}:作业{index}",
content=f"作业{index}的正式说明",
)
for index in range(1, 15)
]
constraint = PromptService._list_completeness_constraint("原生里的作业都有哪些", chunks)
assert "本轮共召回 14 个正式编号练习" in constraint
assert "必须覆盖下面全部 14 项" in constraint
assert "不得新增清单外的作业" in constraint
assert "练习1作业1" in constraint
assert "练习14作业14" in constraint
def test_complete_section_includes_child_headings_but_stops_at_next_peer():
with _database() as db:
knowledge = _add_published_knowledge(db, knowledge_id=1, name="合一课程")