fix:针对回答依据不准的调整
This commit is contained in:
parent
8fb76a9cb6
commit
0b6c4afb77
|
|
@ -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__
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
Loading…
Reference in New Issue