fix:针对回答依据不准的调整

This commit is contained in:
shijing 2026-07-23 10:01:57 +08:00
parent 8fb76a9cb6
commit 0b6c4afb77
2 changed files with 72 additions and 31 deletions

View File

@ -182,11 +182,12 @@ def stream_chat(
# 只传角色码时,其他单位的专属文档也会被 ADP 一并召回喂给 LLM,LLM 会在正文写"依据文档:《XX 单位规定》";
# 把整条链一起下发,是为了让本单位无对应制度时,上级单位的制度也在候选内,由 LLM 按"就近优先"选用.
# 兄弟单位(不在向上链条上的)始终不进 allowed_tags,不会被召回.超管不过滤.
unit_chain_list = build_unit_chain(current_user) if not current_user.is_superuser else []
if current_user.is_superuser:
allowed_tags: list[str] = []
else:
allowed_tags = [role.code for role in current_user.roles]
allowed_tags.extend(build_unit_chain(current_user))
allowed_tags.extend(unit_chain_list)
allowed_tags.extend(["所有人可见", "通用", "全体员工"])
# role_hint 必须是业务岗位(如"总院中层副职""员工"),用来匹配制度里按职级分组的条款。
@ -298,15 +299,19 @@ def stream_chat(
# 硬约束:兜底 ADP 检索层过滤失效 -> 强制 LLM 拒用 unit_chain 之外单位的专属文档.
# 允许 unit_chain 内的上级单位文档(就近优先由上面 fell_back/chosen_unit 分支引导).
# 优先级最高,凌驾于 FAQ 命中等所有规则.命中 FAQ 但 FAQ 内容显然只覆盖链外单位时,也必须拒答.
allowed_units_str = "".join(f"{u}" for u in unit_chain_list) if unit_chain_list else f"{own_unit}"
hint_lines.append(
f"【单位隔离硬约束·最高优先级】提问人所属单位:{own_unit}"
f"允许的单位标签白名单(精确匹配,禁止子串/字面前缀扩展):{allowed_units_str}"
f"允许引用的文档必须满足以下之一:"
f"(a) 标签值等于{own_unit};"
f"(b) 标签值为{own_unit}的上级单位(逐级向上);"
f"(c) 标签值为'通用/全体员工/所有人可见'等公开范围。"
f"(a) 标签值精确等于白名单中任一单位名;"
f"(b) 标签值为'通用/全体员工/所有人可见'等公开范围。"
f"特别注意:标签名之间的字面前缀相同不代表存在上下级关系。"
f"例如'中国建材总院母公司''中国建材总院北京分公司'虽同以'中国建材总院'开头,"
f"但两者是平级独立单位,非上下级关系,禁止互相引用对方专属文档。"
f"不满足则:"
f"(1) 禁止将其内容用于作答,禁止在'依据文档'中列出;"
f"(2) 若召回文档全部不满足(即本单位及其上级链上均无对应制度),必须回复原文:"
f"(2) 若召回文档全部不满足(即白名单单位及公开范围均无对应制度),必须回复原文:"
f"'知识库中未找到 {own_unit} 及其上级单位对应的规定,建议咨询本单位财务部门。';"
f"(3) 此规则覆盖'FAQ 命中即返回标准答案'的短路,即使命中 FAQ 也须先判定单位适用性。"
)
@ -318,6 +323,19 @@ def stream_chat(
)
else:
adp_question = raw_question
# 兜底:allowed_titles 是空集(public_only 且公开也无匹配) -> 单位链上和公开都没文档可用.
# 直接返回硬约束里那句拒答话术, 不再走 ADP 生成, 避免 LLM 自行发挥引用兄弟单位文档.
refuse_directly = (
not is_admin
and own_unit
and isinstance(allowed_titles, set)
and len(allowed_titles) == 0
)
refuse_text = (
f"知识库中未找到 {own_unit} 及其上级单位对应的规定,建议咨询本单位财务部门。"
if refuse_directly else ""
)
def event_stream():
answer_parts: list[str] = []
usage = {"token_usage": 0, "pu_usage": 0}
@ -326,32 +344,40 @@ def stream_chat(
stream_start = datetime.utcnow()
failed = False
error_message: str | None = None
if refuse_directly:
answer_parts.append(refuse_text)
yield f"event: answer.delta\ndata: {json.dumps(refuse_text, ensure_ascii=False)}\n\n"
yield f"event: answer.usage\ndata: {json.dumps(usage, ensure_ascii=False)}\n\n"
yield f"event: answer.done\ndata: {json.dumps({}, ensure_ascii=False)}\n\n"
try:
for event in AdpClient().stream_answer(
adp_question,
user_id=str(current_user_id),
conversation_id=adp_conversation_id,
allowed_tags=allowed_tags,
image_urls=image_urls,
):
event_name = event["event"]
if event_name == "answer.delta":
answer_parts.append(event["data"])
elif event_name == "answer.reference":
title = (event["data"].get("source_title") or "").strip()
if not title:
continue
# 按用户所属单位链过滤引用allowed_titles=None 时不过滤
if allowed_titles is not None and title not in allowed_titles:
continue
key = (title, (event["data"].get("snippet") or "").strip())
if key in seen_refs:
continue
seen_refs.add(key)
references.append(event["data"])
elif event_name == "answer.usage":
usage.update(event["data"])
yield f"event: {event_name}\ndata: {json.dumps(event['data'], ensure_ascii=False)}\n\n"
if not refuse_directly:
for event in AdpClient().stream_answer(
adp_question,
user_id=str(current_user_id),
conversation_id=adp_conversation_id,
allowed_tags=allowed_tags,
unit_name=own_unit,
unit_chain=unit_chain_list,
image_urls=image_urls,
):
event_name = event["event"]
if event_name == "answer.delta":
answer_parts.append(event["data"])
elif event_name == "answer.reference":
title = (event["data"].get("source_title") or "").strip()
if not title:
continue
# 按用户所属单位链过滤引用allowed_titles=None 时不过滤
if allowed_titles is not None and title not in allowed_titles:
continue
key = (title, (event["data"].get("snippet") or "").strip())
if key in seen_refs:
continue
seen_refs.add(key)
references.append(event["data"])
elif event_name == "answer.usage":
usage.update(event["data"])
yield f"event: {event_name}\ndata: {json.dumps(event['data'], ensure_ascii=False)}\n\n"
except Exception as exc: # noqa: BLE001
failed = True
error_message = str(exc) or exc.__class__.__name__

View File

@ -277,6 +277,8 @@ class AdpClient:
user_id: str | None = None,
conversation_id: str | None = None,
allowed_tags: list[str] | None = None,
unit_name: str | None = None,
unit_chain: list[str] | None = None,
image_urls: list[str] | None = None,
) -> Iterator[dict]:
if self._settings.adp_provider == "tencent":
@ -285,6 +287,8 @@ class AdpClient:
user_id=user_id or "anonymous",
conversation_id=conversation_id or uuid.uuid4().hex,
allowed_tags=allowed_tags or [],
unit_name=unit_name or "",
unit_chain=unit_chain or [],
image_urls=image_urls or [],
)
else:
@ -318,6 +322,8 @@ class AdpClient:
user_id: str,
conversation_id: str,
allowed_tags: list[str],
unit_name: str = "",
unit_chain: list[str] | None = None,
image_urls: list[str] | None = None,
) -> Iterator[dict]:
s = self._settings
@ -329,8 +335,17 @@ class AdpClient:
if not text:
text = "请回答这张图片相关的问题。"
contents: list[dict] = [{"Type": "text", "Text": text}]
custom_vars: dict[str, str] = {}
if allowed_tags:
contents.append({"Type": "custom_variables", "CustomVariables": {"allowed_tags": ",".join(allowed_tags)}})
custom_vars["allowed_tags"] = ",".join(allowed_tags)
# unit_name / unit_chain 是 ADP 系统 prompt 里"单位隔离硬约束"的判定依据.
# 不传等于让 LLM 靠字面前缀猜, 会把"中国建材总院母公司"误认成"中国建材总院北京分公司"的上级.
if unit_name:
custom_vars["unit_name"] = unit_name
if unit_chain:
custom_vars["unit_chain"] = ",".join(unit_chain)
if custom_vars:
contents.append({"Type": "custom_variables", "CustomVariables": custom_vars})
body = {
"RequestId": uuid.uuid4().hex,
"ConversationId": conversation_id,