feat: 行程规划改用 tool-calling agent,旧逻辑保留为降级方案

ai.ts 新增通用 runAgentLoop(),blindboxPlanGen.ts 拆分为
agent 主路径(list_ideas/search_poi/finalize_plan 三个工具)
和 legacy 降级路径,agent 失败时自动回退。
AI 思考和工具调用实时推送给前端。
This commit is contained in:
2026-03-02 01:07:25 +08:00
parent 93499867d5
commit 7fd1005e03
2 changed files with 447 additions and 49 deletions
+134
View File
@@ -1,4 +1,8 @@
import OpenAI from "openai";
import type {
ChatCompletionMessageParam,
ChatCompletionTool,
} from "openai/resources/chat/completions";
import type { IdeaTags, PlanItem } from "@/types";
function getClient() {
@@ -224,3 +228,133 @@ ${Object.entries(ctx.candidates)
return null;
}
}
// ---------------------------------------------------------------------------
// Generic tool-calling agent loop
// ---------------------------------------------------------------------------
export interface AgentTool {
name: string;
description: string;
parameters: Record<string, unknown>; // JSON Schema for the tool parameters
execute: (args: Record<string, unknown>) => Promise<string>;
progressBefore?: (args: Record<string, unknown>) => string;
progressAfter?: (args: Record<string, unknown>, result: string) => string;
}
export interface AgentLoopOptions {
systemPrompt: string;
userPrompt: string;
tools: AgentTool[];
onProgress?: (message: string) => void;
maxTurns?: number;
}
export interface AgentLoopResult {
finalArgs: Record<string, unknown>;
turns: number;
}
export async function runAgentLoop(
options: AgentLoopOptions,
): Promise<AgentLoopResult | null> {
const { systemPrompt, userPrompt, tools, onProgress, maxTurns = 15 } = options;
const client = getClient();
const openaiTools: ChatCompletionTool[] = tools.map((t) => ({
type: "function" as const,
function: {
name: t.name,
description: t.description,
parameters: t.parameters,
},
}));
const toolMap = new Map(tools.map((t) => [t.name, t]));
const messages: ChatCompletionMessageParam[] = [
{ role: "system", content: systemPrompt },
{ role: "user", content: userPrompt },
];
for (let turn = 0; turn < maxTurns; turn++) {
const response = await client.chat.completions.create({
model: "deepseek-chat",
messages,
tools: openaiTools,
temperature: 0.6,
max_tokens: 4096,
});
const choice = response.choices[0];
if (!choice) break;
const assistantMessage = choice.message;
// Push AI thinking text to the user
if (assistantMessage.content) {
onProgress?.(assistantMessage.content);
}
// No tool calls — conversation ended
if (!assistantMessage.tool_calls || assistantMessage.tool_calls.length === 0) {
// Add the assistant message and break if there's nothing more to do
messages.push({ role: "assistant", content: assistantMessage.content ?? "" });
if (choice.finish_reason === "stop") break;
continue;
}
// Add the assistant message (with tool_calls) to history
messages.push(assistantMessage as ChatCompletionMessageParam);
// Execute each tool call
for (const toolCall of assistantMessage.tool_calls) {
if (toolCall.type !== "function") continue;
const fnName = toolCall.function.name;
const tool = toolMap.get(fnName);
if (!tool) {
messages.push({
role: "tool",
tool_call_id: toolCall.id,
content: JSON.stringify({ error: `Unknown tool: ${fnName}` }),
});
continue;
}
let args: Record<string, unknown>;
try {
args = JSON.parse(toolCall.function.arguments);
} catch {
args = {};
}
// Progress: before execution
if (tool.progressBefore) {
onProgress?.(tool.progressBefore(args));
}
// Execute
const result = await tool.execute(args);
// Progress: after execution
if (tool.progressAfter) {
onProgress?.(tool.progressAfter(args, result));
}
// If this is finalize_plan, return immediately
if (fnName === "finalize_plan") {
return { finalArgs: args, turns: turn + 1 };
}
// Feed tool result back to the conversation
messages.push({
role: "tool",
tool_call_id: toolCall.id,
content: result,
});
}
}
// Exhausted turns without finalize
return null;
}