feat: trust explicit user goals

This commit is contained in:
wuyang
2026-07-29 16:58:59 +08:00
parent 953f69e092
commit 151941af64
5 changed files with 70 additions and 5 deletions
+2
View File
@@ -15,6 +15,8 @@ Agent 使用 DeepSeek 的思考模式,结合最近的连续原文与精简想
- 不要求用户建立会话;系统在后台保留滚动的连续语境。 - 不要求用户建立会话;系统在后台保留滚动的连续语境。
- 后台的“想法”是广义的演化脉络,也包括项目、工作、学习、问题和现实承诺。 - 后台的“想法”是广义的演化脉络,也包括项目、工作、学习、问题和现实承诺。
- 后续输入让一个方向成形时,系统可以把先前暂存的原文补回完整证据链。 - 后续输入让一个方向成形时,系统可以把先前暂存的原文补回完整证据链。
- 用户明确说这是自己的目标或方向时,系统默认相信并跟踪;语气、野心和暂时没有路径
不构成静默丢弃的理由。
- 成熟度是可回退的连续位势,不是阶段、成绩或任务完成百分比。 - 成熟度是可回退的连续位势,不是阶段、成绩或任务完成百分比。
- 运动、张力和可能动作由模型结合上下文动态生成,不使用固定关卡。 - 运动、张力和可能动作由模型结合上下文动态生成,不使用固定关卡。
- 用户手动校准的位势优先展示,AI 估计仍独立保留。 - 用户手动校准的位势优先展示,AI 估计仍独立保留。
+15 -1
View File
@@ -29,7 +29,7 @@ from .db import Database
from .schemas import CuratorDecision from .schemas import CuratorDecision
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
PROMPT_VERSION = "maturity-2026-07-29.v4" PROMPT_VERSION = "maturity-2026-07-29.v5"
RECENT_CONTEXT_LIMIT = 12 RECENT_CONTEXT_LIMIT = 12
IDEA_CATALOG_LIMIT = 40 IDEA_CATALOG_LIMIT = 40
@@ -51,6 +51,8 @@ CURATOR_INSTRUCTIONS = """
0—100 只是连续的“位势坐标”,不是阶段、成绩或完成百分比。成熟度可以后退;新材料也可能 0—100 只是连续的“位势坐标”,不是阶段、成绩或完成百分比。成熟度可以后退;新材料也可能
让一个看似成熟的想法重新打开。禁止用笔记数量、时间、链接数、是否列清单来机械打分。 让一个看似成熟的想法重新打开。禁止用笔记数量、时间、链接数、是否列清单来机械打分。
位势也绝不是对用户能力、品格或成功概率的评分。目标规模大、困难、像玩笑、暂时没有方案,
都不能成为降低对用户信任或拒绝跟踪的理由;只描述这个想法本身已经长出了什么。
motion 是你对当下运动的自然语言压缩,例如“向现实探去”“重构中”“暂时沉淀”,不使用 motion 是你对当下运动的自然语言压缩,例如“向现实探去”“重构中”“暂时沉淀”,不使用
预设流水线。position 说明此刻所处的位置。trajectory 只描述最近发生的变化。possible_moves 预设流水线。position 说明此刻所处的位置。trajectory 只描述最近发生的变化。possible_moves
@@ -65,6 +67,17 @@ motion 是你对当下运动的自然语言压缩,例如“向现实探去”
不值得跟踪的杂讯。只有纯粹随手事实、情绪或转瞬观察,既无可辨认对象也无未来张力,或信息 不值得跟踪的杂讯。只有纯粹随手事实、情绪或转瞬观察,既无可辨认对象也无未来张力,或信息
少到无法诚实命名时,才 standalone。 少到无法诚实命名时,才 standalone。
你的默认姿态是“有根据的信任”:相信用户主动写下的方向值得被认真对待,同时不做空洞吹捧。
用户拥有自己话语的严肃程度;不要根据口语、幽默、夸张、野心或你对可行性的猜测,擅自把它
解释成“随口说说”。当用户明确把某件事称为自己的目标、想法、项目、工作、计划、问题、
研究、决定或承诺时,direction_signal 必须是 explicit,最终不能 standalone;除非用户自己
明确否定它。即使没有期限、方法或下一步,也应建立低位势脉络:缺少的部分进入 position 和
tension,不能成为拒绝创建的门槛。
保持诚实,不做空洞吹捧。你可以直说一个目标缺乏路径、远离现实或存在巨大矛盾,也可以给出
很低的位势;但这些判断只能描述想法当前的状态,不能反过来否定用户已经宣告的方向、推断用户
只是开玩笑,或把它静默丢弃。possible_moves 应提供现实接触点,而不是用任务惩罚用户。
谨慎合并。仅因主题词相似不能归到同一想法;关注它们是否共享同一个问题、张力或生成方向。 谨慎合并。仅因主题词相似不能归到同一想法;关注它们是否共享同一个问题、张力或生成方向。
一个片段最多关联两个想法。新建必须克制,但克制意味着不凭关键词重复建,不意味着压制真实 一个片段最多关联两个想法。新建必须克制,但克制意味着不凭关键词重复建,不意味着压制真实
的新方向。新建想法时 idea_id 必须为 null;更新时必须使用工具返回的准确 id。 的新方向。新建想法时 idea_id 必须为 null;更新时必须使用工具返回的准确 id。
@@ -717,6 +730,7 @@ class Curator:
user_id, user_id,
"decision_committed", "decision_committed",
{ {
"direction_signal": decision.direction_signal,
"standalone": decision.standalone, "standalone": decision.standalone,
"reasoning_note": decision.reasoning_note, "reasoning_note": decision.reasoning_note,
"assessments": assessments, "assessments": assessments,
+22 -2
View File
@@ -1,6 +1,6 @@
from __future__ import annotations from __future__ import annotations
from typing import Annotated from typing import Annotated, Literal
from pydantic import ( from pydantic import (
BaseModel, BaseModel,
@@ -36,7 +36,14 @@ class IdeaAssessment(BaseModel):
) )
title: str = Field(min_length=1, max_length=80) title: str = Field(min_length=1, max_length=80)
summary: str = Field(min_length=1, max_length=280) summary: str = Field(min_length=1, max_length=280)
maturity: float = Field(ge=0, le=100) maturity: float = Field(
ge=0,
le=100,
description=(
"Position of the evolving idea itself, never a score of the user or "
"a probability of success."
),
)
confidence: float = Field(ge=0, le=1) confidence: float = Field(ge=0, le=1)
motion: str = Field(min_length=1, max_length=24) motion: str = Field(min_length=1, max_length=24)
position: str = Field(min_length=1, max_length=120) position: str = Field(min_length=1, max_length=120)
@@ -73,6 +80,15 @@ class IdeaAssessment(BaseModel):
class CuratorDecision(BaseModel): class CuratorDecision(BaseModel):
direction_signal: Literal["explicit", "implicit", "none"] = Field(
description=(
"explicit when the user names this as their goal, idea, project, "
"work, plan, question, inquiry, decision, or commitment; implicit "
"when a future-bearing direction is inferred; none when no such "
"direction is present. Tone, ambition, feasibility, and missing "
"steps must not downgrade an explicit signal."
)
)
standalone: bool = Field( standalone: bool = Field(
description="True when the fragment should remain only in the raw stream for now." description="True when the fragment should remain only in the raw stream for now."
) )
@@ -84,6 +100,10 @@ class CuratorDecision(BaseModel):
@model_validator(mode="after") @model_validator(mode="after")
def decision_is_coherent(self) -> "CuratorDecision": def decision_is_coherent(self) -> "CuratorDecision":
if self.direction_signal == "explicit" and self.standalone:
raise ValueError(
"an explicit user-declared direction cannot remain standalone"
)
if self.standalone and self.assessments: if self.standalone and self.assessments:
raise ValueError("standalone decisions cannot contain assessments") raise ValueError("standalone decisions cannot contain assessments")
if not self.standalone and not self.assessments: if not self.standalone and not self.assessments:
+2 -1
View File
@@ -44,7 +44,7 @@ def test_tool_loop_uses_no_tool_convergence_repair(
return SimpleNamespace( return SimpleNamespace(
final_output=( final_output=(
'{"standalone":true,"reasoning_note":"暂时独立保留",' '{"standalone":true,"reasoning_note":"暂时独立保留",'
'"assessments":[]}' '"direction_signal":"none","assessments":[]}'
) )
) )
@@ -100,6 +100,7 @@ def test_curator_recovers_visible_prior_fragment(
return SimpleNamespace( return SimpleNamespace(
final_output=( final_output=(
"{" "{"
'"direction_signal":"explicit",'
'"standalone":false,' '"standalone":false,'
'"reasoning_note":"明确的新工作,应形成可跟踪脉络",' '"reasoning_note":"明确的新工作,应形成可跟踪脉络",'
'"assessments":[{' '"assessments":[{'
+29 -1
View File
@@ -1,8 +1,16 @@
from pathlib import Path from pathlib import Path
import pytest
from pydantic import ValidationError
from app.config import UserSeed from app.config import UserSeed
from app.curator import _rank_ideas, build_curator_prompt from app.curator import (
CURATOR_INSTRUCTIONS,
_rank_ideas,
build_curator_prompt,
)
from app.db import Database from app.db import Database
from app.schemas import CuratorDecision
ADMIN = UserSeed( ADMIN = UserSeed(
@@ -128,6 +136,26 @@ def test_curator_prompt_contains_recent_continuity_and_idea_catalog(
assert "是否形成新的未完成方向" in prompt assert "是否形成新的未完成方向" in prompt
def test_explicit_user_direction_cannot_be_silently_discarded():
with pytest.raises(
ValidationError,
match="explicit user-declared direction cannot remain standalone",
):
CuratorDecision.model_validate(
{
"direction_signal": "explicit",
"standalone": True,
"reasoning_note": "目标太大,暂时不值得跟踪",
"assessments": [],
}
)
assert "有根据的信任" in CURATOR_INSTRUCTIONS
assert "用户拥有自己话语的严肃程度" in CURATOR_INSTRUCTIONS
assert "不能成为拒绝创建的门槛" in CURATOR_INSTRUCTIONS
assert "保持诚实,不做空洞吹捧" in CURATOR_INSTRUCTIONS
def test_later_fragment_can_recover_earlier_standalone_evidence( def test_later_fragment_can_recover_earlier_standalone_evidence(
tmp_path: Path, tmp_path: Path,
): ):