Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
01d006d55f | ||
|
|
60082532a9 | ||
|
|
fb51081750 | ||
|
|
4efcf83e4c | ||
|
|
1398938437 |
@@ -1,6 +1,6 @@
|
||||
# 那拉提文旅智能问答
|
||||
|
||||
Vue 3 + TypeScript + Vite 前端;FastAPI + Pydantic + SQLAlchemy 后端;PostgreSQL + pgvector。当前后端提供知识管理、来源、文档、人工审核、关键词/向量/RRF 检索、文本与向量发布、历史记录及认证/角色权限,问答仍为占位接口。
|
||||
Vue 3 + TypeScript + Vite 前端;FastAPI + Pydantic + SQLAlchemy 后端;PostgreSQL + pgvector。当前后端提供知识管理、来源、文档、人工审核、关键词/向量/RRF 检索、文本与向量发布、历史记录及认证/角色权限,`/api/chat` 提供条目级关键词证据检索,不生成模型答案。
|
||||
|
||||
## 启动数据库
|
||||
需要可用的 Docker Engine(Windows 可使用 Docker Desktop),在项目根目录运行:
|
||||
@@ -48,7 +48,7 @@ Vite 通常使用 http://localhost:5173。
|
||||
- 注册/登录/退出、当前会话、资料修改、改密与管理员用户列表,详见 [认证接口契约](docs/AUTH_API.md)
|
||||
- GET `/api/health`
|
||||
- GET `/api/ready`:检查数据库连接
|
||||
- POST `/api/chat`:占位接口,尚未接入检索和 LLM
|
||||
- POST `/api/chat`:已核验条目关键词检索,问题1–500字符;返回 retrieval_only/no_evidence 和来源,详见 [CHAT_API.md](docs/CHAT_API.md)
|
||||
- GET `/api/knowledge`
|
||||
- POST `/api/knowledge`
|
||||
- GET `/api/knowledge/page`、GET/PATCH/DELETE `/api/knowledge/{id}`
|
||||
@@ -107,7 +107,7 @@ Vite 通常使用 http://localhost:5173。
|
||||
|
||||
前端包括问答、知识库管理、来源与更新、实验评估四个页面。实验评估页面没有虚构数据;所有页面明确标记当前功能状态。
|
||||
|
||||
注意:当前已实现本地 Embedding、文本/向量快照、精确向量及 RRF 混合检索;`/api/chat` 仍是占位接口,生成模型实际调用暂缓,网络搜索、后台更新任务和真实领域评测尚未实现。管理接口由服务端认证与角色校验;审核表单的核验人仍是人工输入字段。已完成隔离环境的浏览器 Cookie/CSRF、角色与真实检索核心流程验收,见 [浏览器验收记录](docs/BROWSER_ACCEPTANCE.md) 与 [导出/采集时间复核](docs/EXPORT_BROWSER_ACCEPTANCE.md);完整生产部署、Docker 数据库与 HTTPS 验收仍待完成。`.env` 不要提交到 Git。
|
||||
注意:当前已实现本地 Embedding、文本/向量快照、精确向量及 RRF 混合检索;`/api/chat` 已接入独立关键词检索,生成模型实际调用暂缓,网络搜索、后台更新任务和真实领域评测尚未实现。管理接口由服务端认证与角色校验;审核表单的核验人仍是人工输入字段。已完成隔离环境的浏览器 Cookie/CSRF、角色与真实检索核心流程验收,见 [浏览器验收记录](docs/BROWSER_ACCEPTANCE.md) 与 [导出/采集时间复核](docs/EXPORT_BROWSER_ACCEPTANCE.md);完整生产部署、Docker 数据库与 HTTPS 验收仍待完成。`.env` 不要提交到 Git。
|
||||
|
||||
### 后端测试和迁移回滚
|
||||
|
||||
|
||||
@@ -26,9 +26,6 @@ def chat(payload: ChatRequest, db: Session = Depends(get_db)) -> ChatResponse:
|
||||
"因此系统仅返回可追溯证据,不生成或改写答案。"
|
||||
),
|
||||
status="retrieval_only",
|
||||
sources=[
|
||||
{"title": hit.title, "source_url": hit.source_url}
|
||||
for hit in result.results
|
||||
],
|
||||
sources=[{"title": hit.title, "source_url": hit.source_url} for hit in result.results],
|
||||
retrieval_run_id=result.run_id,
|
||||
)
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
from typing import Literal
|
||||
from uuid import UUID
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
from pydantic import BaseModel
|
||||
|
||||
from app.schemas.catalog import Input
|
||||
from app.schemas.retrieval import QueryText
|
||||
|
||||
|
||||
class ChatRequest(BaseModel):
|
||||
question: str = Field(min_length=1, max_length=2000)
|
||||
class ChatRequest(Input):
|
||||
question: QueryText
|
||||
|
||||
|
||||
class SourcePreview(BaseModel):
|
||||
|
||||
@@ -1,14 +1,16 @@
|
||||
from datetime import datetime
|
||||
from typing import Literal
|
||||
from typing import Annotated, Literal
|
||||
from uuid import UUID
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
from app.schemas.catalog import Category, Input
|
||||
|
||||
QueryText = Annotated[str, Field(min_length=1, max_length=500)]
|
||||
|
||||
|
||||
class RetrievalRequest(Input):
|
||||
query: str = Field(min_length=1, max_length=500)
|
||||
query: QueryText
|
||||
mode: Literal["keyword"] = "keyword"
|
||||
top_k: int = Field(default=5, ge=1, le=20)
|
||||
category: Category | None = None
|
||||
|
||||
@@ -0,0 +1,161 @@
|
||||
"""Retrieval-only chat integration; synthetic evidence, never a generation call."""
|
||||
|
||||
import copy
|
||||
|
||||
import pytest
|
||||
from fastapi.testclient import TestClient
|
||||
from sqlalchemy import func, select, text
|
||||
from sqlalchemy.orm import Session
|
||||
from test_catalog import document, source
|
||||
from test_retrieval import add_item
|
||||
from test_runtime import server
|
||||
|
||||
from app.models.rag import RagContext
|
||||
from app.models.retrieval import RetrievalRun
|
||||
from app.services import retrieval
|
||||
|
||||
|
||||
def test_overlong_chat_question_is_rejected_at_the_http_boundary(client):
|
||||
with TestClient(
|
||||
client.app, base_url="https://testserver", raise_server_exceptions=False
|
||||
) as public:
|
||||
response = public.post("/api/chat", json={"question": "q" * 501})
|
||||
assert response.status_code == 422, response.text
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"payload",
|
||||
[
|
||||
{},
|
||||
{"question": ""},
|
||||
{"question": " \t\n "},
|
||||
{"question": None},
|
||||
{"question": 123},
|
||||
{"question": "q" * 2000},
|
||||
{"question": "q" * 2001},
|
||||
{"question": "alpha", "model": "not-accepted"},
|
||||
{"question": "alpha", "messages": []},
|
||||
],
|
||||
)
|
||||
def test_invalid_chat_input_creates_no_retrieval_or_context(client, migrated_database, payload):
|
||||
response = client.post("/api/chat", json=payload)
|
||||
assert response.status_code == 422, response.text
|
||||
assert all("input" not in error for error in response.json()["detail"])
|
||||
with Session(migrated_database) as db:
|
||||
assert db.scalar(select(func.count()).select_from(RetrievalRun)) == 0
|
||||
assert db.scalar(select(func.count()).select_from(RagContext)) == 0
|
||||
|
||||
|
||||
def test_chat_query_boundary_normalization_and_public_history_permissions(client):
|
||||
response = client.post("/api/chat", json={"question": "q" * 500})
|
||||
assert response.status_code == 200, response.text
|
||||
assert response.json()["status"] == "no_evidence"
|
||||
client.cookies.clear()
|
||||
public = client.post("/api/chat", json={"question": " alpha "})
|
||||
assert public.status_code == 200
|
||||
assert public.json()["status"] == "no_evidence" and public.json()["sources"] == []
|
||||
assert client.get(f"/api/retrieval/runs/{public.json()['retrieval_run_id']}").status_code == 401
|
||||
|
||||
|
||||
def test_chat_sources_match_audited_snapshot_and_history_stays_immutable(client, migrated_database):
|
||||
approved = add_item(client, title="verified alpha", content="alpha synthetic evidence")
|
||||
add_item(client, status="pending", title="pending alpha", content="alpha unreviewed")
|
||||
add_item(client, status="rejected", title="rejected alpha", content="alpha rejected")
|
||||
deleted = add_item(client, title="deleted alpha", content="alpha deleted")
|
||||
assert (
|
||||
client.delete(
|
||||
f"/api/knowledge/{deleted['id']}?expected_version={deleted['version']}"
|
||||
).status_code
|
||||
== 204
|
||||
)
|
||||
result = client.post("/api/chat", json={"question": " alpha "}).json()
|
||||
assert result["status"] == "retrieval_only" and "不生成" in result["answer"]
|
||||
history = client.get(f"/api/retrieval/runs/{result['retrieval_run_id']}").json()
|
||||
assert history["query"] == "alpha" and history["mode"] == "keyword"
|
||||
assert [h["knowledge_id"] for h in history["results"]] == [approved["id"]]
|
||||
assert result["sources"] == [
|
||||
{"title": h["title"], "source_url": h["source_url"]} for h in history["results"]
|
||||
]
|
||||
saved = copy.deepcopy(history)
|
||||
assert (
|
||||
client.patch(
|
||||
f"/api/knowledge/{approved['id']}",
|
||||
json={"expected_version": approved["version"], "content": "revised alpha"},
|
||||
).status_code
|
||||
== 200
|
||||
)
|
||||
assert client.post("/api/chat", json={"question": "alpha"}).json()["status"] == "no_evidence"
|
||||
assert client.get(f"/api/retrieval/runs/{history['run_id']}").json() == saved
|
||||
assert (
|
||||
client.post(
|
||||
"/api/rag/contexts",
|
||||
json={"retrieval_run_id": history["run_id"], "completion": {"model": "contract-model"}},
|
||||
).status_code
|
||||
== 422
|
||||
)
|
||||
with Session(migrated_database) as db:
|
||||
assert db.scalar(select(func.count()).select_from(RagContext)) == 0
|
||||
|
||||
|
||||
@pytest.mark.parametrize("change", ["source", "document"])
|
||||
def test_chat_cannot_reuse_evidence_after_catalog_revision(client, change):
|
||||
src = source(client)
|
||||
doc = document(client, src["id"])
|
||||
add_item(client, document_id=doc["id"], content="alpha synthetic catalog evidence")
|
||||
assert client.post("/api/chat", json={"question": "alpha"}).json()["status"] == "retrieval_only"
|
||||
path, version, fields = (
|
||||
(f"sources/{src['id']}", src["version"], {"source_url": "https://example.com/new"})
|
||||
if change == "source"
|
||||
else (f"documents/{doc['id']}", doc["version"], {"content": "revised synthetic document"})
|
||||
)
|
||||
assert (
|
||||
client.patch("/api/" + path, json={"expected_version": version, **fields}).status_code
|
||||
== 200
|
||||
)
|
||||
result = client.post("/api/chat", json={"question": "alpha"}).json()
|
||||
assert result["status"] == "no_evidence" and result["sources"] == []
|
||||
|
||||
|
||||
def test_chat_capacity_failure_is_not_a_successful_empty_answer(
|
||||
client, migrated_database, monkeypatch
|
||||
):
|
||||
add_item(client, content="alpha synthetic capacity evidence")
|
||||
monkeypatch.setattr(retrieval, "MAX_ITEMS", 0)
|
||||
response = client.post("/api/chat", json={"question": "alpha"})
|
||||
assert response.status_code == 503 and "sources" not in response.json()
|
||||
with Session(migrated_database) as db:
|
||||
assert db.scalar(select(func.count()).select_from(RetrievalRun)) == 0
|
||||
|
||||
|
||||
def test_chat_failed_history_insert_does_not_leave_partial_rows(client, migrated_database):
|
||||
with migrated_database.begin() as db:
|
||||
db.execute(
|
||||
text("ALTER TABLE retrieval_runs ADD CONSTRAINT reject_chat_run CHECK(false) NOT VALID")
|
||||
)
|
||||
assert client.post("/api/chat", json={"question": "alpha"}).status_code == 409
|
||||
with Session(migrated_database) as db:
|
||||
assert db.scalar(select(func.count()).select_from(RetrievalRun)) == 0
|
||||
|
||||
|
||||
def test_chat_retrieval_history_survives_real_http_restart(migrated_database):
|
||||
with server(migrated_database) as client:
|
||||
assert client.post("/api/chat", json={"question": "q" * 501}).status_code == 422
|
||||
item = add_item(client, content="alpha real HTTP synthetic evidence")
|
||||
response = client.post("/api/chat", json={"question": " alpha "})
|
||||
assert response.status_code == 200
|
||||
result = response.json()
|
||||
history = client.get(f"/api/retrieval/runs/{result['retrieval_run_id']}").json()
|
||||
assert history["results"][0]["knowledge_id"] == item["id"]
|
||||
assert history["query"] == "alpha"
|
||||
with server(migrated_database) as client:
|
||||
assert client.get(f"/api/retrieval/runs/{result['retrieval_run_id']}").json() == history
|
||||
exported = client.post(
|
||||
"/api/retrieval/exports",
|
||||
json={"run_ids": [result["retrieval_run_id"]], "include_contexts": True},
|
||||
)
|
||||
assert exported.status_code == 200
|
||||
client.cookies.clear()
|
||||
assert (
|
||||
client.post("/api/chat", json={"question": "alpha"}).json()["status"]
|
||||
== "retrieval_only"
|
||||
)
|
||||
@@ -14,7 +14,7 @@ FastAPI + Pydantic
|
||||
|-- 来源获取 -> 人工核验 -> 文本切分 -> 生成向量 -> 发布版本
|
||||
```
|
||||
|
||||
当前后端实现健康/数据库就绪检查、来源与文档管理、知识增删改查、分页筛选、人工审核、历史快照、认证会话/角色权限、文本/向量快照及独立关键词/向量/RRF 检索;问答仍为占位接口。数据库由 Alembic 显式迁移并启用 pgvector,DeepSeek RAG 待明确恢复后实现。前端已接入登录/注册、个人中心、账户设置、会话恢复、路由守卫和角色菜单,以及来源、文档的创建、编辑、软删除、分页搜索与历史快照;实验页不伪造数据,知识审核与浏览器 Cookie/PostgreSQL 联调仍需验收。
|
||||
当前后端实现健康/数据库就绪检查、来源与文档管理、知识增删改查、分页筛选、人工审核、历史快照、认证会话/角色权限、文本/向量快照及独立关键词/向量/RRF 检索;`/api/chat` 为独立条目关键词检索适配,返回检索状态与来源,不调用生成模型(见 [CHAT_API.md](CHAT_API.md))。数据库由 Alembic 显式迁移并启用 pgvector,DeepSeek RAG 待明确恢复后实现。前端已接入登录/注册、个人中心、账户设置、会话恢复、路由守卫和角色菜单,以及来源、文档的创建、编辑、软删除、分页搜索与历史快照;实验页不伪造数据,知识审核与浏览器 Cookie/PostgreSQL 联调仍需验收。
|
||||
|
||||
## 阶段 2 数据边界
|
||||
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
# 检索式聊天接口
|
||||
|
||||
`POST /api/chat` 是已核验知识的条目级关键词检索适配,不调用生成模型。它与问答页面直接使用的 chunk/RRF 接口不同,也不会创建 Responses 准备快照。生成模型与真实服务接入继续延期。
|
||||
|
||||
## 请求
|
||||
|
||||
```json
|
||||
{"question":"那拉提示例关键词"}
|
||||
```
|
||||
|
||||
接口公开可用,无需登录;只接受 question。服务端去除首尾空白后校验1–500个字符,与底层 RetrievalRequest 共用同一字段约束;字符数不代表模型token数。缺失、空白、错误类型、超长输入或额外字段(包括 model、messages、凭据)均返回422,不写检索记录。只有停用词或标点、没有有效检索词时同样返回422。错误不包含原始输入值。
|
||||
|
||||
原适配曾允许2000字符,内层检索只允许500字符,导致501–2000字符请求返回500。现在该范围在HTTP入参处明确返回422,不截断问题,也不绕过原检索限制。
|
||||
|
||||
## 响应与证据
|
||||
|
||||
成功返回 answer、status、sources 和 retrieval_run_id。answer 为固定的检索状态说明,**不是生成式回答**。
|
||||
|
||||
- status=retrieval_only:返回最多5条来源预览(title/source_url),来自同一条目关键词检索历史的排名结果。说明中的命中总数可以大于来源预览数;完整排名、正文片段、来源/知识版本、配置、语料指纹和耗时保存在原检索记录中。
|
||||
- status=no_evidence:没有命中的有效已核验依据,sources为空;仍保存该次真实无命中检索的run ID。
|
||||
|
||||
待审核、拒绝、软删除、来源/文档失效或修订后等待重新审核的条目不作为来源。资料修订影响后续请求,不重写已经保存的历史快照。没有LLM调用量、生成答案或费用字段,不能把固定说明用于领域质量评分。
|
||||
|
||||
检索历史及JSONL导出仍要求管理员权限;公开获取run ID不授予历史读取权限。该run的mode=keyword、证据粒度为knowledge_item,不能传入只接收chunk检索的 `POST /api/rag/contexts`。需要Responses准备时先使用chunks/vector/hybrid检索。
|
||||
|
||||
容量超限/数据库不可用保留503,历史保存约束冲突返回409;错误不会伪装成200无命中,也不会保存部分历史。仅成功完成检索与历史事务后返回来源。无额外数据库迁移。
|
||||
|
||||
## 验证边界
|
||||
|
||||
测试使用合成条目、真实隔离PostgreSQL和默认应用HTTP生命周期,覆盖500字符边界、501字符HTTP失败复现与修正、参数拒绝不写记录、公开调用与历史权限、审核筛选、资料修订/历史不变、容量与事务失败,以及进程重启后的历史和导出。没有模型调用或新增浏览器验收。
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
本阶段按用户要求先实现通用请求格式,生成模型选择和真实调用延期。新准备请求采用非流式 **Responses API**(服务端点为 `<base_url>/responses`):`model/instructions/input/max_output_tokens`,从结构化 `output` 读取 assistant 文本。依据 [OpenAI Responses 迁移说明](https://developers.openai.com/api/docs/guides/migrate-to-responses)、[DeepSeek Responses 兼容表](https://api-docs.deepseek.com/guides/responses_api/) 与[百炼 Responses 文档](https://help.aliyun.com/zh/model-studio/qwen-api-via-openai-responses)。当前仍为纯组包和解析,不绑定实际服务地址、密钥或模型,不发出 HTTP 生成请求。
|
||||
|
||||
本阶段提供管理员上下文准备与历史接口、完整证据快照、固定提示词及纯响应解析器。`POST /api/chat` 仍保留原占位状态,尚未成为生成式问答接口;准备记录固定为 `kind=prepared_request, model_called=false`,不能作为实际模型回答、耗时、费用或领域实验结果。
|
||||
本阶段提供管理员上下文准备与历史接口、完整证据快照、固定提示词及纯响应解析器。`POST /api/chat` 是独立的条目级关键词检索适配,返回检索状态和来源,尚未成为生成式问答接口(见 [CHAT_API.md](CHAT_API.md));准备记录固定为 `kind=prepared_request, model_called=false`,不能作为实际模型回答、耗时、费用或领域实验结果。
|
||||
|
||||
## 准备与读取
|
||||
|
||||
|
||||
+10
-2
@@ -11,9 +11,9 @@
|
||||
| T05 | 知识库 | 设计 sources/documents/chunks/version 表 | 范涵宇 | 已完成(迁移、发布快照和接口已合入;待 Docker 联调) |
|
||||
| T06 | 检索 | 实现关键词检索基线 | 范涵宇 | 已完成(已核验知识 BM25 与检索历史已合入) |
|
||||
| T07 | 向量 | 评估 Embedding 并实现 pgvector | 范涵宇 | 已完成(本地模型、精确向量和 RRF 已合入;待真实领域评测) |
|
||||
| T08 | RAG | 集成 LLM 与来源展示 | 范涵宇 | 协议基础已合入(Responses 请求/解析调整已在独立分支通过验证,待审;生成模型选型与实际调用延期) |
|
||||
| T08 | RAG | 集成 LLM 与来源展示 | 范涵宇 | Responses 协议已合入(请求/解析与旧快照兼容已完成;生成模型选型与实际调用延期) |
|
||||
| T09 | 实验 | 构建可核验问题集及评分规范 | 杨星萱 | 待办 |
|
||||
| T10 | 工程 | 补充测试、Alembic、鉴权与部署 | 范涵宇(主)、全员 | 进行中(迁移、认证、CSRF、224项回归及浏览器核心流程已通过;Docker/HTTPS/部署待验收) |
|
||||
| T10 | 工程 | 补充测试、Alembic、鉴权与部署 | 范涵宇(主)、全员 | 进行中(迁移、认证、CSRF及浏览器核心流程已通过;聊天边界修正和312项分支回归待审,Docker/HTTPS/部署待验收) |
|
||||
|
||||
## 2026-10-15 首次联调记录
|
||||
|
||||
@@ -126,3 +126,11 @@
|
||||
- 本轮 `pytest -q backend/tests/test_rag.py backend/tests/test_exports.py` **113 passed**;完整 `pytest -q backend/tests` **294 passed, 1 skipped**。skip 为显式启用的真实 Embedding 专项,本轮不运行模型。ruff检查/格式检查及前端类型/生产构建通过;未修改页面交互,不宣称新增浏览器验收。
|
||||
|
||||
- PR #44 合入后同步最新 dev,保留两份阶段记录并解决本文件追加冲突。此次同步后重新执行前端时区6项、会话3项及类型/生产构建,均通过;backend树与此前294项回归时一致,本次未重复运行全量后端测试,也未新增模型调用或浏览器验收。
|
||||
|
||||
|
||||
## 2026-10-17 T10:检索式聊天边界与后端验收
|
||||
|
||||
- PR #45 的 head/merge 均已核对进入 dev,再从最新 dev 创建 `fix/backend-chat-validation`。新合入 PR #46 已实现 `/api/chat` 的检索式适配,本轮补上后端边界修正和集成验收,不重复实现检索、不接入生成模型。
|
||||
- 已复现501字符问题返回500:外层ChatRequest允许2000字符,内层RetrievalRequest只允许500字符。现在两者共用QueryText与输入规范,问题去首尾空白后1–500字符,超长/空白/错误类型/额外字段在HTTP边界返回422,不静默截断,也不写检索历史。
|
||||
- 补充有效审核来源筛选、资料修订后重新审核、公开聊天/私有历史权限、历史与导出持久化、容量503及写入409完整回滚验证;真实HTTP重启后检索历史保持一致。Chat仅生成固定检索状态说明,条目级记录不能传给chunk级Responses准备。完整契约见 [CHAT_API.md](CHAT_API.md)。
|
||||
- 专项 `pytest -q backend/tests/test_chat.py backend/tests/test_retrieval.py` **35 passed**;完整 `pytest -q backend/tests` **312 passed, 1 skipped**,skip为显式启用的真实Embedding专项,本轮未运行模型。ruff检查/格式检查、diff检查和最新前端类型/生产构建通过。API路径、响应字段和数据库迁移不变;README/架构文档更正已过期的聊天占位说明。无新增浏览器验收或领域分数。
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
# 资料追溯字段核对(阶段1 · 2026-10-17)
|
||||
|
||||
> 用途:核对资料台账所需字段与系统(后端数据模型 + 前端页面)实际字段的对应关系,确认是否满足「来源可回访、核验可追溯」的要求,并给出需补齐的字段建议。
|
||||
> 对照对象:[资料台账](material-ledger.md) 的字段 vs 后端 `knowledge_items` / `sources` / `documents` 三张表,以及前端知识管理、来源管理、知识审核页面。
|
||||
> 结论:核心追溯字段已基本覆盖,存在 5 处需补齐/确认的差异。
|
||||
|
||||
## 一、字段对照表
|
||||
|
||||
| 台账字段 | 系统对应字段(表) | 覆盖情况 |
|
||||
|---|---|---|
|
||||
| 编号 | `id`(自动生成) | ✅ |
|
||||
| 标题 | `title`(knowledge / documents) | ✅ |
|
||||
| 类别 | `category`(knowledge / documents,默认「未分类」) | ✅ |
|
||||
| 来源名称 | `sources.name`(经 `knowledge.source_id` 关联) | ⚠️ 见差异 1 |
|
||||
| URL | `source_url`(knowledge / sources) | ✅ |
|
||||
| 获取日期 | `collected_at`(documents 必填,knowledge 可空) | ⚠️ 见差异 2 |
|
||||
| 更新时间 | `updated_at`(自动维护) | ✅ 系统有,台账此前缺此列,已补 |
|
||||
| 正文摘要 | `content`(正文,最长 10 万字符) | ⚠️ 见差异 3 |
|
||||
| 真实性判断 | `verification_notes`(核验说明) | ✅ |
|
||||
| 核验日期 | `reviewed_at` | ✅ |
|
||||
| 核验人 | `reviewer` | ✅ |
|
||||
| 状态 | `verification_status`(pending / verified / rejected) | ✅ 三值与台账完全一致 |
|
||||
| 备注 | 无独立字段(sources / documents 有 `notes`,knowledge 无) | ⚠️ 见差异 4 |
|
||||
|
||||
## 二、差异与建议
|
||||
|
||||
1. **来源名称是独立实体**:知识条目本身只有 `source_url`,来源名称需先在「来源管理」登记 `sources`,再通过 `source_id` 关联。前端知识管理页目前只展示「来源网址」链接,未显示来源名称,也未在知识表单暴露 `source_id` 选择。建议:知识录入时允许选择已登记来源(`source_id`),并在列表同时显示来源名称与网址。
|
||||
|
||||
2. **采集时间在知识条目上可空**:`documents.collected_at` 必填、`knowledge.collected_at` 可空。前端知识表单已有「采集时间」输入,但后端允许为空。为满足追溯,建议知识条目也要求填写采集时间,或强制关联到含采集时间的文档。
|
||||
|
||||
3. **「正文摘要」无独立字段**:系统只有正文 `content`,无单独摘要列。台账中「正文摘要」作为登记用摘要;正式入库时以 `content`(正文/摘要原文)为准,两者须在核验时对齐,避免摘要与正文不一致。
|
||||
|
||||
4. **知识条目缺通用「备注」**:`knowledge_items` 没有 `notes`,只有 `verification_notes`(核验说明)。若需在审核之外记录备注(如过期时间、待二次确认),目前只能写入核验说明或关联文档的 `notes`。建议:给知识条目增加 `notes` 字段(需范涵宇评估,仅建议,不直接改库)。
|
||||
|
||||
5. **来源名称回填**:现有直接填 `source_url` 的条目,来源名称缺失;后续入库前应补登记到 `sources` 并关联。
|
||||
|
||||
## 三、核对结论
|
||||
|
||||
- 追溯核心字段(标题、类别、URL、获取/更新时间、核验状态、核验人、核验依据)系统均已支持,状态三值与审核规则一致。
|
||||
- 主要缺口集中在「来源名称的独立登记与前端展示」「知识条目通用备注」「摘要与正文的一致性」三点,需在阶段 2 入库前与范涵宇、吉海燕确认。
|
||||
- 数据库结构改动仅为建议,不由本文档直接执行。
|
||||
@@ -19,21 +19,23 @@
|
||||
|---|---|---|
|
||||
| 编号 | 是 | 唯一序号,格式 `K-0001`,按登记顺序递增 |
|
||||
| 标题 | 是 | 资料的简短标题,便于检索和引用 |
|
||||
| 类别 | 是 | 四选一:红色文化 / 旅游资源 / 民宿服务 / 交通出行 |
|
||||
| 来源名称 | 是 | 发布方,如机构/媒体/网站名称 |
|
||||
| URL | 是 | 来源链接;无法溯源时填「无链接」并在真实性判断中说明 |
|
||||
| 类别 | 是 | 四选一:红色文化 / 旅游资源 / 民宿服务 / 交通出行;测试条目用「其他」并注明测试 |
|
||||
| 来源名称 | 是 | 发布方,如机构/媒体/网站名称;无来源的测试条目填「无(测试)」 |
|
||||
| URL | 否 | 来源链接;无法溯源时留空并说明 |
|
||||
| 获取日期 | 是 | 采集日期,格式 `YYYY-MM-DD` |
|
||||
| 更新时间 | 是 | 最近一次更新该条目的日期 `YYYY-MM-DD` |
|
||||
| 正文摘要 | 是 | 资料关键内容的一两句话摘要,不改变原意 |
|
||||
| 真实性判断 | 是 | 核验结论依据,如「与官方公告一致」「与另一独立来源交叉印证」「无法核实」 |
|
||||
| 真实性判断 | 是 | 核验结论依据,如「与官方公告一致」「与另一独立来源交叉印证」「无法核实」;测试条目填「无需核验(测试)」 |
|
||||
| 核验日期 | 否 | 完成核验的日期 `YYYY-MM-DD`;`pending` 时可为空 |
|
||||
| 核验人 | 否 | 完成核验的人;`pending` 时可为空 |
|
||||
| 状态 | 是 | `pending` / `verified` / `rejected` |
|
||||
| 备注 | 否 | 补充说明,如过期时间、需二次确认的事项 |
|
||||
| 备注 | 否 | 补充说明,如过期时间、需二次确认的事项、测试标记 |
|
||||
|
||||
## 登记表
|
||||
|
||||
| 编号 | 标题 | 类别 | 来源名称 | URL | 获取日期 | 正文摘要 | 真实性判断 | 核验日期 | 核验人 | 状态 | 备注 |
|
||||
|---|---|---|---|---|---|---|---|---|---|---|---|
|
||||
| K-0001 | 【示例·占位】某景区简介 | 旅游资源 | 【示例来源,非真实】示例站点 | https://example.com/ | 2026-10-15 | 【示例】此处为占位摘要,非真实资料 | 待核验 | | | pending | 示例行,仅演示填写格式,不构成真实资料 |
|
||||
| 编号 | 标题 | 类别 | 来源名称 | URL | 获取日期 | 更新时间 | 正文摘要 | 真实性判断 | 核验日期 | 核验人 | 状态 | 备注 |
|
||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
||||
| K-0001 | 【示例·占位】某景区简介 | 旅游资源 | 【示例来源,非真实】示例站点 | https://example.com/ | 2026-10-15 | 2026-10-15 | 【示例】此处为占位摘要,非真实资料 | 待核验 | | | pending | 示例行,仅演示填写格式,不构成真实资料 |
|
||||
| K-0002 | 阶段1联调测试条目 | 其他(测试) | 无(测试) | | 2026-10-17 | 2026-10-17 | 仅用于验证资料登记与持久化流程,不构成任何那拉提文旅事实 | 无需核验(测试) | | | pending | 阶段1测试条目,仅验证登记流程,不进入正式检索库,不伪装为真实资料 |
|
||||
|
||||
> 上表仅含一条**示例占位行**,用于演示填写格式,不包含任何真实文旅信息。正式收集的资料按此格式逐行追加。
|
||||
> 上表 K-0001 为**示例占位行**,K-0002 为**测试条目**,均不包含真实文旅信息。正式收集的资料按此格式逐行追加。
|
||||
|
||||
Reference in New Issue
Block a user