96SEO 2026-08-08 07:05 3
很多 AI Agent Demo 都有一个共同特点:跑起来很顺。话说回来,

使用者点按钮。后端调模型,模型返回结果,页面展示。
这个流程适合演示。
但一旦把 Agent 做进真实业务程序。问题会立刻变得具体:
模型跑了 秒,页面要不要继续等?使用者点取消,是直接杀掉任务,还是等安全点停止?生成提案失败了点重试会不会重复生成?服务重启后running 任务怎么收尾?任务从质检页、工作台、提案页发起,最终在哪里统一查看?
这些问题看起来不如 Prompt、Function Calling、模型路由那么“AI”。
但真正做过一轮之后我反而觉得:
上一篇我讲了 AgentTask 和 AgentStep更偏后端状态机:任务怎么落库。步骤怎么记录,worker 怎么 claim,lease 怎么续租。其实,
这一篇继续往前走一步。讲 状态机怎么变成使用者可感知、可操作的产品体验。话说回来,
我现在对 Agent 长任务体验的设计原则是:
| 原则 | 对应工程动作 |
|---|---|
| 状态必须可见 | 前端持续展示任务状态和步骤进度 |
| 取消必须可控 | running 任务只写取消信号。在安全点停止 |
| 重试必须幂等 | 每次重试使用稳定 operation key |
| 恢复必须有策略 | 不同任务类型使用不同中断恢复策略 |
| 任务必须集中管理 | 工作台面板 + 独立任务中心统一承接 |
对应到程序里就是几条链路:
| 链路 | 关键设计 |
|---|---|
| 前端轮询只在任务活跃、页面可见时轮询 | |
| 任务面板把任务状态和可操作按钮展示给使用者 | |
| 取消命令 pending → 立即取消;running → 写取消信号 | |
| 重试命令保留旧任务,创建 successor;提案重试必须带当前章节 revision,避免基于过期正文继续生成 | |
| 中断恢复:不同任务有不同恢复策略;怎么说呢,任务中心统一查看写作、修复、质检、记忆等多类任务 | |
If previous article talked about “Agent 怎么跑”,this article answers:
The typical short‑task UI looks like:
按钮:生成 状态:生成中 结果:生成成功 / 生成失败
This works for quick calls。but an Agent is a **long‑running** workflow. In a novel‑writing scenario a single task may involve:
读取作品上下文 → 检查章节计划 → 推荐写作技能 → 生成草稿 → 连续性审核 → 创建提案 → 等待使用者应用 → 定稿后提取记忆
If UI only shows “AI 正在生成”,users quickly lose feeling of control. They don’t know wher system is reading context,calling a model,stuck,or safely pausing. They also worry about:
This is *** long‑task experience is **not** just a UI tweak;it’s part of Agent engineering.
The backend persists task status;front‑end can poll it. However blind polling leads to useless requests when:
A shared hook centralises polling logic:
// Define active statuses
const ACTIVE_STATUSES =;function hasActiveTask {
return tasks.some);}
// Detect page visibility
function usePageVisible {
const = useState(
=> document.visibilityState!== 'hidden'
);useEffect => {
const update = => setIsVisible;document.addEventListener;return => document.removeEventListener;},),return isVisible;}
...
export function useAgentTasks {
const queryClient = useQueryClient;const queryKey = workspaceQueryKeys.agentTasks;const isPageVisible = usePageVisible;const query = useQuery({
queryKey,queryFn: async => ).data。enabled: novelId> 0,refetchInterval: =>
isPageVisible && hasActiveTask?POLL_MS : false,});// ,老实说,expose cancel/retry mutations…}
This hook ensures:
User needs actionable buttons。not raw enums. The panel maps each status to permissible actions:
| Status / 状态码 | User Action / 可执行操作 | `pending` / `running` | Cancel | `pending_review` / `failed` / `cancelled` / `interrupted` | Retry | `completed` | — |
|---|
// Status ↔︎ Text mapping
const STATUSTEXT = {
pending: '等待中',pendingreview: '待审核',running: '执行中',completed: '已完成',failed: '失败',cancelled: '已取消',interrupted: '已中断',};
// Helper predicates function isActive { return .includes;} function canRetry { return .includes;}
// Render buttons based on status export function WorkspaceTaskPanel { return (
当前章节暂无任务
} {tasks.map(task=>({task.error}
}A cancel click must be recorded in backend so that running workers can stop at ir next safe point.
// Front‑end mutation
const cancelMutation = useMutation({
mutationFn: => agentApi.cancel,onSuccess: invalidate,});
@router.post
async def cancelagenttask(
taskid:int,idempotencykey:str|None=Header,db:AsyncSession=Depends,sessionfactory=Depends):
至于try,return await cancelagenttaskcommand(db,taskid,operationkey=idempotencykey,sessionfactory=sessionfactory)
except (AgentTaskCommandConflict,AgentTaskCommandNotFound。AgentTaskCommandValidationError) as err:
_raisecommand_error
python async def cancelagenttaskcommand: fingerprint = _fingerprint replay = await _replayreceipt if replay: return replay
task = await db.get if not task: raise AgentTaskCommandNotFound
now = datetime.utcnow # Immediate cancellation if not yet running immediate = await db.execute( update .where(AgentTask.id==taskid,AgentTask.status!='running',AgentTask.status.notin) .values(status='cancelled',cancelrequestedat=now,finishedat=now,dedupekey=None)) if immediate.rowcount ==0: # Running – just mark request time await db.execute( update .where(AgentTask.id==task_id。AgentTask.status=='running') .values) await db.commit cancelled = await db.get if not cancelled: raise AgentTaskCommandNotFound return cancelled
\
\
// Generate stable key
export function newAgentTaskRetryKey {
return typeof crypto?.randomUUID === 'function'
crypto.randomUUID
: retry-${Date.now}-${Math.random.toString.slice};}
// Retrier that caches keys per original task ID export function createAgentTaskRetrier{ const retryKeys=new Map;return{ async retry{ let payload={};const opKey=retryKeys.get||createRetryKey;retryKeys.set;不过,const result=await agentApi.retry;老实说,retryKeys.delete;return result;} },}
| Scenario<\/th> | Operation Key Strategy<\/th><\/\/t r><\/ ad >\ |
|---|
\
\
tsx export const PROPOSALCHAPTERREQUIRED='提案缺少关联章节';export const PROPOSALREVISIONREQUIRED='无法获取当前章节版本';
type ProposalRevisionResolver==>Promise
export function createAgentTaskRetrier{
const retryKeys=new Map
\
\
python
@classmethod
async def recover_startup:
"""Mark legacy running tasks as interrupted and reconcile leases."""
async with session_factory as s:
now=datetime.utcnow
legacy=await s.scalars.where(
AgentTask.status=='running'。AgentItem.task_type.not_in))
for t in legacy:
t.status='interrupted'
t.error='服务重启导致中断,请重新发起'
t.finished_at=now
t.lease_token=None;t.lease_expires_at=None
# mark steps failed …await s.commit
dispatcher=AgentDispatcher
return len+await dispatcher.recover_expired
python
async def recover_expired:
async with self._session_factory as s:
expired_tasks=list.where(
AgentTask.status=='running',AgentTicket.lease_expires_at
Task Type Recovery Strategy Reason \
writechapterproposal interrupted + proposal marked failed user must decide re‑run
<\/ td><\/ tr\
memory_extract back to pending <\/ td> low risk auto‑retry
<\/ td><\/ tr\
ors interrupted + error note
\
\
\
python MEMORYRETRYBACKOFF_SECONDS=
async def runmemory: completed=await runmemoryclaim if completed:return True await self.schedulememory_retry;return False
async def schedulememoryretry: async with self.sessionfactory as sess: task=await sess.get if not task or task.tasktype!='memoryextract' or ... :return idx=task.retrygeneration if idx>=len:return delay=MEMORYRETRYBACKOFFSECONDS successor=await buildagent_task+timedelta) sess.add;await sess.commit
The workspace panel only shows tasks for *current* chapter. A full system may spawn many independent agents:
\
/* 同步加载选中的 Task 步骤 /
tsx
useEffect=>{if{loadSteps}else setSteps},);
/ 活跃时持续刷新 /
tsx
useEffect=>{
if)return;按理说,const timer=setInterval=>{loadTasks;怎么说呢,loadSteps;},5000),return=>clearInterval;},),
/ 步骤进度计算 */
tsx
function taskProgress{
const total=steps.length;ifreturn isActive?0:100,const done=steps.filter).length;return Math.round*100);}
作为专业的SEO优化服务提供商,我们致力于通过科学、系统的搜索引擎优化策略,帮助企业在百度、Google等搜索引擎中获得更高的排名和流量。我们的服务涵盖网站结构优化、内容优化、技术SEO和链接建设等多个维度。
| 服务项目 | 基础套餐 | 标准套餐 | 高级定制 |
|---|---|---|---|
| 关键词优化数量 | 10-20个核心词 | 30-50个核心词+长尾词 | 80-150个全方位覆盖 |
| 内容优化 | 基础页面优化 | 全站内容优化+每月5篇原创 | 个性化内容策略+每月15篇原创 |
| 技术SEO | 基本技术检查 | 全面技术优化+移动适配 | 深度技术重构+性能优化 |
| 外链建设 | 每月5-10条 | 每月20-30条高质量外链 | 每月50+条多渠道外链 |
| 数据报告 | 月度基础报告 | 双周详细报告+分析 | 每周深度报告+策略调整 |
| 效果保障 | 3-6个月见效 | 2-4个月见效 | 1-3个月快速见效 |
我们的SEO优化服务遵循科学严谨的流程,确保每一步都基于数据分析和行业最佳实践:
全面检测网站技术问题、内容质量、竞争对手情况,制定个性化优化方案。
基于用户搜索意图和商业目标,制定全面的关键词矩阵和布局策略。
解决网站技术问题,优化网站结构,提升页面速度和移动端体验。
创作高质量原创内容,优化现有页面,建立内容更新机制。
获取高质量外部链接,建立品牌在线影响力,提升网站权威度。
持续监控排名、流量和转化数据,根据效果调整优化策略。
基于我们服务的客户数据统计,平均优化效果如下:
我们坚信,真正的SEO优化不仅仅是追求排名,而是通过提供优质内容、优化用户体验、建立网站权威,最终实现可持续的业务增长。我们的目标是与客户建立长期合作关系,共同成长。
Demand feedback