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