feat(consult): answer the sentence asked — no motive guessing, follow-up turns answer directly, first turn says each thing once (BUG-1070~1073)

- Voice: contrast limited to chart structures; drop 「这句我按『……』理解了」; forbid motive/need sentences; yes/no answered first (BUG-1070)
- Follow-up turn (session already holds an answer): ≤200-char direct answer, no skeleton; decided from stored history, no intent regex; tool still runs each turn (BUG-1071)
- Thinking bar first section 「先回答你问的这件事」 (BUG-1072)
- First turn: opener without actions, body headings 盘里支持 / 时间怎么看 / 这周可以做的一件事 only, actions once, ≤900 chars (BUG-1073, D8)
- Checklist is a list to check, not paragraphs to write (T3)

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0199rbQDTsUbCVw84wc8BTFe
This commit is contained in:
Jesse_Chen
2026-09-27 23:31:33 +08:00
co-authored by Claude Fable 5.1
parent 2ce3c2165c
commit 9a42333a55
18 changed files with 416 additions and 70 deletions
+17 -7
View File
@@ -44,8 +44,9 @@ import { consultationPublicActivityEvent, streamAgentResponse } from "@/lib/stre
import type { AgentExecutionReceipt, WorkflowReceipt } from "@/lib/consultation-agent-events";
import {
consultationContinueMessages,
dailyAnswerShapeInstruction,
natalAnswerShapeInstruction,
followUpAnswerShapeInstruction,
hasPriorAssistantAnswer,
natalUserTurnShape,
natalConsultationThinkingPlan,
type PublicThinkingSection,
} from "@/lib/consultation-thinking-plan";
@@ -991,9 +992,11 @@ export async function POST(request: Request) {
: "如需新的个人星盘结论,必须调用服务器绑定的排盘工具。";
// The loop's own step after the calculation writes the answer (BUG-1053),
// so the answer shape the removed compose prompt used to carry is here.
const natalInstruction = consultEntrypoint === "daily_starlanguage"
? `${natalToolInstruction}${dailyAnswerShapeInstruction()}`
: `${natalToolInstruction}${natalAnswerShapeInstruction()}`;
// A session that already holds an answer gets the follow-up shape
// (BUG-1071): a direct answer, no skeleton. That is read from the stored
// history, never from the wording of the question.
const followUpTurn = hasPriorAssistantAnswer(history);
const natalInstruction = `${natalToolInstruction}${natalUserTurnShape({ entrypoint: consultEntrypoint, history })}`;
const adoptedRangeNote = consultationMode === "verified_chart"
&& prepared.serverChart?.truth.birthTimeStatus === "accepted"
&& prepared.serverChart.toolInput.candidate_range
@@ -1311,6 +1314,7 @@ export async function POST(request: Request) {
if (!prepared.serverChart) throw new Error("server_chart_truth_missing");
state.thinkingPlan = natalConsultationThinkingPlan({
domains: [consultationTheme],
followUp: followUpTurn,
});
const agentContext = createConsultationAgentContext({
userId,
@@ -1320,6 +1324,7 @@ export async function POST(request: Request) {
plan: prepared.preReserveResult,
theme: consultationTheme,
entrypoint: consultEntrypoint,
followUpTurn,
serverChart: prepared.serverChart,
abortSignal: agentAbortSignal,
state,
@@ -1511,6 +1516,11 @@ export async function POST(request: Request) {
{ birthTimeSource: prepared.serverChart.truth.birthTimeSource },
);
const workflowReceipt = consultationWorkflowReceipt(workflowContext);
// The grounded fallback writes from the injected workflow; same first-turn
// / follow-up split as the agentic path (BUG-1071), one statement of it.
const legacyNatalInstruction = hasPriorAssistantAnswer(history)
? followUpAnswerShapeInstruction()
: "先用不超过 400 字口语直接回答下面的问题,不要加标题;形状为:先给反差(表面 A,底下 B,都是盘上的结构,命名成一个格局)、再说清谁在推谁在修、然后「别去应 X 的象,去扮演 Y 的象」;开场不写行动。然后按三个二级标题写:## 盘里支持这个判断的地方(原始结构、六步宫位、Yoga 表作为依据,只写开场没说过的,表格与正文二选一)、## 时间怎么看、## 这周可以做的一件事(最多三条行动,只在这里出现一次)。全文不超过 900 字。骨架不可省略;不写「先回答你的问题」这个标题,文末技法审计表由产品界面折叠展示,不写进正文。星盘事实只使用系统里已经注入的计算结果,不要复述内部字段、JSON 或再跑一遍咨询流程。";
const cacheBoundary = cachedSystemMessage("【上下文缓存边界】后续内容为本轮请求输入。", selectedModel.model);
const legacyHistory = history.map((message) => message.role === "user"
@@ -1527,7 +1537,7 @@ export async function POST(request: Request) {
content: consultationUserTurnContent({
currentTime: currentTimeContext(requestTime),
name,
instruction: "先用不超过 400 字口语直接回答下面的问题,不要加标题;形状为:先给反差(表面 A,底下 B,命名成一个格局)、再说清谁在推谁在修、然后「别去应 X 的象,去扮演 Y 的象」、收尾最多三条短行动(破折号短句,每条不超过 20 字);然后再按 skill Level 2 骨架写:原始结构、六步宫位、Yoga 表、时机、综合、文末技法审计表,最后才是现代生活。骨架不可省略。星盘事实只使用系统里已经注入的计算结果,不要复述内部字段、JSON 或再跑一遍咨询流程。",
instruction: legacyNatalInstruction,
summaryText: historyWindow.summaryText,
droppedCount: historyWindow.droppedCount,
question: resolvedQuestion.modelQuestion,
@@ -1552,7 +1562,7 @@ export async function POST(request: Request) {
content: consultationUserTurnContent({
currentTime: currentTimeContext(requestTime),
name,
instruction: "先用不超过 400 字口语直接回答下面的问题,不要加标题;形状为:先给反差(表面 A,底下 B,命名成一个格局)、再说清谁在推谁在修、然后「别去应 X 的象,去扮演 Y 的象」、收尾最多三条短行动(破折号短句,每条不超过 20 字);然后再按 skill Level 2 骨架写:原始结构、六步宫位、Yoga 表、时机、综合、文末技法审计表,最后才是现代生活。骨架不可省略。星盘事实只使用系统里已经注入的计算结果,不要复述内部字段、JSON 或再跑一遍咨询流程。",
instruction: legacyNatalInstruction,
summaryText: historyWindow.summaryText,
droppedCount: historyWindow.droppedCount,
question: resolvedQuestion.modelQuestion,