AI 资讯
Designing Puzzle Hints Around Blockers, Not Tap Sequences
A weak puzzle walkthrough records every input. A stronger one explains why the board refuses to move. That distinction matters in traffic-sorting puzzles, where a correct tap can still be useless if a garage exit, crossing lane, or temporary holding space remains blocked. I used Car Sort level 13 as a small case study for a better hint model. The useful unit is not “tap car number seven.” It is a dependency: this vehicle cannot leave until that lane opens; that lane cannot open until a matching garage accepts its front car. Model the board as dependencies The visible board can be represented as a directed graph. Cars and blockers are nodes. An edge from A to B means A must move before B becomes actionable. The graph does not need to reproduce the game engine. It only needs to describe the decisions a player can verify on screen. type MoveNode = { id : string color : string blockedBy : string [] releases : string [] checkpoint : string } This structure makes a hint resilient. If a player has already cleared one harmless car, the guide can still say, “restore the center exit, then release the stack behind it.” A memorized tap list often becomes useless as soon as the board differs by one move. Separate release moves from cleanup moves Puzzle solvers tend to treat every successful departure as equal. They are not equal. A release move changes the dependency graph by opening a lane or exposing a buried color. A cleanup move removes a car that was already free. Good guidance labels those roles explicitly. The player should know whether the current move creates new options or merely reduces clutter. That is especially useful on compact boards, where an attractive matching car may tempt the player even though it does not improve the central bottleneck. The reserved route for this analysis is documented as Car Sort puzzle help . The value of that page is its focus on visible blockers and release points rather than an unexplained command stream. Add visual checkpoints After
AI 资讯
How to Fix 'NoneType' Object Has No Attribute Errors (Without Guessing)
Your script crashes, and near the bottom of the traceback sits AttributeError: 'NoneType' object has no attribute 'name' . It reads like Python is being deliberately unhelpful — but it's actually telling you something precise. You just tried to use a variable that turned out to be None , and it's telling you exactly which one and where. The error isn't saying your program is fundamentally broken. It's saying: at this exact line, you reached for an attribute on a value that was None instead of the object you expected. That's a narrow claim, and once you know how to read it, tracking down why it was None is usually mechanical. What the error is actually telling you Take this code: class User : def __init__ ( self , id , name ): self . id = id self . name = name def find_user ( users , user_id ): for u in users : if u . id == user_id : return u return None user = find_user ( users , target_id ) print ( user . name ) # AttributeError: 'NoneType' object has no attribute 'name' Read the message in two parts. 'NoneType' object has no attribute 'name' tells you the object you called .name on wasn't a User — it was None . has no attribute 'name' tells you which access failed. Put together: whatever user was pointing to when you hit that line wasn't what you expected — it was nothing at all. The message never claims .name is the problem. .name is just where the crash became visible. The real question is one step earlier: why was user None ? Here, find_user() falls through its loop without a match and explicitly returns None — so either target_id is wrong, or that user genuinely isn't in the list yet. The fix, step by step Read the attribute name in the error ( 'name' here) — that tells you which line and which access failed, nothing more. Trace back to where the None value came from. Find the line that assigned, returned, or fetched it. Ask why it's None there , specifically. The most common causes: a lookup function that found nothing and returned None , a dict.get() call th
AI 资讯
Building a Graph From Tabular Relationship Data
Almost every graph starts life as relational tables. The conversion is mechanical once three decisions are made, and one of the three — id remapping — is a silent correctness bug rather than a matter of taste. Deciding what is a node Start with three tables: customers (customer_id, region, signup_date, tenure_days), products (product_id, category, price), and orders (order_id, customer_id, product_id, amount, ordered_at). The rule that resolves nearly every case: A table with a primary key that other tables point at is a node type. A table whose whole job is to link two keys is an edge type. So customers and products are nodes, and orders are edges — even though orders has its own primary key. The order id is not an entity you want to reason about; it is an identifier for a relationship. The harder case is a repeated categorical column such as region . It can stay a customer feature, or it can become a node type with a customer–region edge. The test is behavioural, not aesthetic: do you want information to flow between rows that share this value? As a feature, region is a tag on each customer and nothing more. As a node, it creates a two-hop path between every pair of customers in the same region, so their representations start blending. If a region contains 400,000 customers, that node is a hub through which everything mixes, which is usually a way of turning four hundred thousand distinct customers into one regional average. Keep high-cardinality-of-membership categoricals as features; promote a category to a node when its membership is small and meaningful. If you end up with more than one node type, the model has to change too — see heterogeneous graph neural networks . The id remapping nobody warns you about Graph libraries do not store your ids. They store a node feature matrix and an edge index of integer positions into it, because a message-passing layer is a gather over rows of a dense array. So node ids must be contiguous integers from 0 to n−1 , per node
AI 资讯
Extracting a Bibliography Into Structured Citation Records
The instinct is to hand the whole reference list to a model and ask for an array of citation objects. On a list of eighty entries that produces seventy-three, with two merged and five hallucinated into tidiness. The fix is to make segmentation a separate, deterministic step. Two stages, and why the first one is harder Parsing one reference string into author, year, title and venue is a task current models do well. Deciding where one reference ends and the next begins is a task they do badly, because the boundary is typographic rather than semantic: a hanging indent, a numeric label, a line break that is either a wrap or a separator depending on the column width. Splitting the work also gives you a count to assert against. If the list is numbered 1 to 84 and you segmented 81 entries, you know the parse is wrong before you have looked at a single field. A single-call extraction gives you no such handle — a merged pair looks identical to a list that was three shorter. Step 1: segment the list Three reference-list styles cover almost everything, and each has a different boundary signal: Numbered (Vancouver, IEEE). Each entry begins with 1. or [1] . Boundary detection is a regex, the sequence is monotonic, and you get the assertion for free. Author-date (APA, Harvard, Chicago author-date). No labels. Entries are separated by a hanging indent — the first line starts at the margin and continuations are indented — which is invisible in a flat text stream and obvious in the layout. Note-bibliography (Chicago notes). Also unlabelled, also hanging-indented, and additionally uses a three-em dash for a repeated first author, which is the case discussed below. For the unlabelled styles, segment on the indent rather than on the text. If you have coordinates from the PDF, an entry starts at every line whose left edge is at the block minimum and continues through every line indented further. If you do not have coordinates, a reasonable proxy is a line that begins with a capital lett
AI 资讯
Gating a Merge on an Eval Score in Azure Pipelines
If your Azure Pipelines eval gate runs on pushes to main but never on a pull request, the YAML is not the problem. Microsoft’s documentation is explicit: for an Azure Repos Git repository you cannot configure a PR trigger in the YAML file, and the functionality is implemented by a branch policy instead. Why your pr trigger does nothing The pr: key exists in the Azure Pipelines YAML schema, and it works — for GitHub and Bitbucket Cloud repositories. For Azure Repos Git it is inert. The Azure Repos Git documentation states that pull request triggers are implemented using branch policies, and that to enable PR validation you configure the Build validation policy on the target branch. A pr: block in the file is not an error and produces no warning; it simply never causes a run. Two related things surprise people once the policy exists. Draft pull requests do not trigger a pipeline even with a branch policy configured, so a gate that seems not to run may be running against a draft. And you must be a project administrator of the project to configure validation builds at all, which is why this is often the step that a developer cannot complete themselves. This is a product behaviour rather than a version detail, but it is the kind of thing that changes. Check the Azure Repos Git page in Microsoft’s Azure Pipelines documentation before assuming it still holds. The pipeline A single-stage pipeline is enough. The CI trigger below covers pushes; the pull request path comes from the policy in the next section, and no pr: key appears at all because on Azure Repos it would only be misleading to a reader. trigger : branches : include : - main paths : exclude : - docs/* pool : vmImage : ubuntu-latest variables : - group : llm-eval-keys - name : EVAL_MODEL value : gpt-4.1-mini-2025-04-14 steps : - task : UsePythonVersion@0 inputs : versionSpec : ' 3.12' - script : pip install -r evals/requirements.txt displayName : Install eval dependencies - script : | python -m evals.run \ --cases
AI 资讯
Authenticating to Azure OpenAI With Managed Identity
The substitution is three lines of client code. The part that costs an afternoon is that the most powerful-looking Azure OpenAI role is explicitly unable to make an inference call. What a key cannot do An Azure OpenAI resource key is a bearer secret with no identity, no expiry and no scope narrower than the whole resource. Every deployment on the resource is reachable with it, every caller looks identical in the audit trail, and rotating it means coordinating every consumer at once. A managed identity replaces it with a short-lived Microsoft Entra ID token issued to a specific workload identity. The credential is never stored, the token expires on its own, and the grant is a role assignment you can scope to a resource group, a resource, or nothing at all. Combined with a private endpoint, it removes the two things an attacker needs — the network path and the static secret. The role that permits inference Microsoft documents four roles for Azure OpenAI, and the summary table on its RBAC article makes one distinction that is worth reading twice: Cognitive Services OpenAI User — can make inference API calls with Microsoft Entra ID. Cannot read or regenerate keys, cannot create deployments, cannot create guardrails. Cognitive Services OpenAI Contributor — everything the User role has, plus creating and editing deployments, fine-tuning and stored completions. Cognitive Services Contributor — can create resources, read and regenerate keys, and create customised guardrails, but is listed as unable to make inference API calls with Microsoft Entra ID . Cognitive Services Usages Reader — quota visibility only, and only at subscription scope. That third entry is the trap. Granting an application the Contributor role because it sounds broader produces an application that can rotate the keys it is no longer using and cannot call the model at all. The role you want for a workload is Cognitive Services OpenAI User , and nothing else. Microsoft also notes that subscription-level Ow
AI 资讯
Building a Multi-Engine 3D Generation API: Routing, Credits, and Webhooks
How I designed the API layer for Trify3D — a platform that routes one input across multiple AI 3D engines (Tripo3D, Meshy, Rodin) so users can compare meshes side by side. This post covers provider routing, async job management with Trigger.dev, idempotency for credit safety, and webhook delivery. The Problem Every AI 3D engine has a blind spot. Tripo3D is fast (~48 seconds) and great at hard-surface props, but it flattens organic detail. Meshy handles characters and creatures more cleanly (~76 seconds), but its topology gets messy on hard surfaces. Rodin produces the highest-fidelity PBR textures (~90 seconds), but it's the slowest and most expensive. If a user picks one engine, they're stuck with its weaknesses. To compare results, they'd need three separate accounts, three subscriptions, and three credit pools — then manually juggle browser tabs. I built Trify3D to solve this: one input, every engine, one credit pool. A user uploads an image or writes a prompt, the platform routes it to multiple 3D AI engines simultaneously, and they compare the meshes side by side before exporting the winner. This post is about the API layer that makes that work. Architecture Overview Here's the high-level flow: Client Request │ ▼ ┌──────────────────┐ │ API Gateway │ Bearer auth, rate limit, idempotency check │ (trify3d.com) │ └────────┬─────────┘ │ ▼ ┌──────────────────┐ │ Provider Router │ Routes to Tripo3D / Meshy / Rodin │ (mode + model) │ based on mode + model prefix └────────┬─────────┘ │ ┌────┼────┐ ▼ ▼ ▼ ┌──────┐┌──────┐┌──────┐ │Tripo3D││Meshy ││Rodin │ Async generation └──┬───┘└──┬───┘└──┬───┘ │ │ │ └───────┼───────┘ ▼ ┌──────────────────┐ │ Trigger.dev │ Job orchestration, retries, 10-min timeout │ (async runner) │ └────────┬─────────┘ │ ┌────┴────┐ ▼ ▼ ┌────────┐ ┌──────────┐ │ Poll │ │ Webhook │ Client picks one or both │ (GET) │ │ (POST) │ └────────┘ └──────────┘ Three decisions drove this architecture: Async-first — 3D generation takes 48–90 seconds. No HTTP reque
AI 资讯
AI Is Removing the Middle Class of Software Engineering
You can prompt an agent for three hours and ship a 25,000-line pull request. Nobody on your team can tell you why it works — or why it breaks at 2 AM. The New Workflow It's 2026. You're the senior engineer on a mid-size product team. Your job has always been the person who catches the architecture mistakes before they compound — the one who notices that a Kafka dependency was grafted onto a read-heavy query, or that someone denormalized the database because it was faster than fixing the ORM. This morning, you open your inbox. There are seven pull requests. The first one is 24,506 lines added, 3,938 removed, with a description that reads: "Implemented user analytics pipeline with event streaming." You pull the branch. It runs. The tests pass. When you ask the author where the data flows, they send you a link to a Claude conversation. Somewhere in that 47-turn exchange, between confident architectural recommendations and polite apologies when the model changed its mind, is the design decision. You read all 47 turns. You still don't know why they chose Kafka. This is not a hypothetical. This is what the post-AI-productivity era looks like for teams that adopted coding agents without updating their engineering discipline. The speed limit has been removed. And the people who built their careers on being the speed limit are now obsolete. What Changed Before AI coding assistants, there was a natural throughput cap on software output. A senior engineer could review perhaps three meaningful pull requests per day. A team of ten could ship maybe fifteen high-quality merges per sprint. This cap wasn't arbitrary — it was enforced by the time required to actually understand what you were merging. AI changed the cost structure, not the review requirement. A developer armed with a capable agent can now produce 25,000 lines of code in a morning. The agent writes the code. The agent writes the tests. The agent writes the documentation. The agent even writes the PR description, which
AI 资讯
SQL Window Functions: How to Get the Top Row Per Group
By the end of this page you can answer the question that stops most people the first week they write SQL: which row is the best one in each category. You will know OVER and PARTITION BY , the three ranking functions and how each treats a tie, a running total, and LAG for comparing a row to the one before it. It is about twenty-five minutes. Here is what to actually do with it. The next time you write GROUP BY genre and get back a best rating without the name attached to it, stop rewriting the GROUP BY . Add ROW_NUMBER() OVER (PARTITION BY genre ORDER BY rating DESC) to the plain query instead, then keep the rows numbered 1. That is the whole move, and it replaces a query most people never get working. The short version: a window function adds a calculated column to each row while leaving every row in place. Grouping collapses rows. A window looks at them. One idea decides everything else on this page, so it gets the picture. Both halves do the same arithmetic over the same four rows, and only one of them still has four rows at the end. The original carries a diagram here. In words: Two panels side by side, each starting from the same stack of four identical row shapes. The left panel is labelled GROUP BY. Its four rows funnel down through a single arrow into one row at the bottom, and the four original rows are shown faded to indicate they are gone from the result. Only one row remains. The right panel is labelled OVER. Its four rows stay exactly where they are, at full strength, and each one gains a small badge on its right hand side holding a number: one, two, three, four. Nothing funnels and nothing is faded. The contrast is the whole idea: the left panel ends with a single summary row and no way to say which original row it came from, while the right panel ends with all four rows still present, each carrying its own calculated value. The worked example is real. Every number on this page comes from a published portfolio project: finding the genuinely overlooked g
AI 资讯
GROUP BY and HAVING: How to Summarize Rows Without Getting a Fake Answer
By the end of this page you can write a summary query and know its answer is real. You will know exactly what GROUP BY does to your rows, which columns you are allowed to select afterwards and why, where WHERE goes, where HAVING goes, and why swapping them is the difference between a finding and a number that means nothing. It is about twenty-five minutes. Here is what to actually do with it. On the next summary query you write, add one line setting a minimum group size before you read the ranking. One line, and it removes the most common way a summary query produces a confident wrong answer. The short version: WHERE filters rows before grouping. HAVING filters groups after. Without a HAVING floor, tiny groups float to the top of every ranking. One idea decides everything else here, so it gets the picture. Grouping happens in the middle of the query, and the two filters sit on opposite sides of it. The original carries a diagram here. In words: A left-to-right pipeline in four stages. Stage one is a column of eight individual row boxes. Stage two is a gate labelled WHERE, through which six rows pass and two are crossed out and stopped. Stage three shows the surviving six rows collapsing into three group boxes, one holding three rows, one holding two rows, and one holding a single row. Stage four is a second gate labelled HAVING, through which the group of three and the group of two pass, while the group holding only one row is crossed out and stopped. The result at the far right is two groups. The picture shows that WHERE acts on individual rows before any grouping exists, and HAVING acts on whole groups after they have been formed, which is why the two filters cannot be swapped. The worked example is real. Every number on this page comes from a published portfolio project: 82,956 games from the Steam catalogue, with review counts, ratings and genres. The queries run against the full dataset at Steam Hidden Gems on GitHub . If SELECT and WHERE are also new, start wi
AI 资讯
SQL Foundations, Start to Finish
By the end of this page you can say, out loud and in your own words, what every core piece of SQL does. What a table and a row really are. The six clauses, and the order they actually run in, which is not the order you type them. NULL , and why it breaks comparisons. Filtering, aggregation, GROUP BY and HAVING . Joins. CASE . Subqueries, CTEs and window functions. Keys and indexes. That list is most of what an analyst job, an interview, and a first real dataset will ask of you. Here is what to actually do with it. Go through once end to end without stopping, just for the shape. Then come back to the retrieval sheet near the bottom, cover the right-hand column, and try to say each answer before you read it. That second pass is where the learning happens, and there is measured evidence for it further down. The short version: SQL is one sentence with six parts, and every part answers a different question about your rows. Learn what each part does and where it runs, and the rest is vocabulary. One idea decides more of your SQL experience than any other, so it gets the picture. You write a query in one order. The database runs it in a different order. Almost every confusing SQL error is that gap. The original carries a diagram here. In words: Two columns of stacked boxes face each other. The left column, headed "you write", lists the clauses in typing order from top to bottom: SELECT, FROM, WHERE, GROUP BY, HAVING, ORDER BY, LIMIT. The right column, headed "it runs", lists the same clauses in execution order: FROM, WHERE, GROUP BY, HAVING, SELECT, ORDER BY, LIMIT. Curved lines connect each clause on the left to the same clause on the right. Six of the seven lines run roughly straight across. One line, the one belonging to SELECT, is drawn in a strong accent color and sweeps steeply downward from the very top of the left column to the fifth position on the right, showing that SELECT is written first but runs almost last, after grouping has already happened. What this page
AI 资讯
Automating Your Morning: A Daily Briefing Pipeline You Can Build
Automating Your Morning: A Daily Briefing Pipeline You Can Build You should not manually read news, emails, or Slack in the morning. The average knowledge worker loses 23 minutes to context switching between 8:00 AM and 9:30 AM, according to a 2023 RescueTime study. That is 92 hours per year—two full workweeks—spent on low-signal input. The fix is not "waking up earlier." The fix is building a passive briefing pipeline that compiles, ranks, and summarizes your information sources before you open your laptop. This article shows you the exact architecture, tools, and failure points, based on my own production setup running for 14 months. The Problem: Your Morning Input Is Unstructured Here is the chain of causality. You wake up and check three things: email, Slack/Teams, and newsfeeds. Each app is a separate silo with its own notification system. Each notification triggers a micro-decision: Is this urgent? Do I need to act? Should I forward this? That decision process is not free. A 2022 University of California Irvine study measured that after each interruption, it takes an average of 23 minutes to return to deep focus. But most people never return to deep focus in the morning—they just bounce between silos. The result is "reactive paralysis": you start your day by responding to others' priorities, not your own. And because each silo sorts by recency (not importance), you read a promotional email from your bank before a critical client update. Why Manual Curation Fails You might think, "I'll just spend 10 minutes skimming." Let me give you the math. If you receive 50 emails, 30 Slack messages, and 20 industry news headlines, that is 100 items. At 6 seconds each to decide relevance (not read), that is 10 minutes of pure triage. But you will read the interesting ones—that is a minimum of 45 minutes total. The deeper issue is recency bias . News apps show you the latest story, not the most important one. Email shows the newest sender, not the highest-value contact. With
AI 资讯
Report or Analysis?
This guide gives you a test that takes ten seconds and tells you whether the thing you just built is a report or an analysis. Then it gives you four moves that turn one into the other. Every move has a worked SQL example and real numbers. The whole method is here. What you actually do: take the number you just produced, and ask what someone would do differently because of it. If the honest answer is nothing, you have a report. Then you run the four moves below, in order, until the answer is a specific action a specific person can take on Monday. The short version. Data analysis is looking at records of things that already happened and finding a pattern that changes what someone does next. If nothing changes, it was not analysis. It was a report. The same starting number, two endings. The test: what would someone do differently? Before you read the answer, look at the last thing you built and try it yourself. Who was going to act on it, and what were they going to do? Take any number you have produced and finish this sentence out loud: "Because of this, someone should do a specific thing ." Both blanks have to fill in with something real. A named person or team, and an action they control. Here is a real one. "Churn was 4.1% in Q3." Who acts, and how? Nobody can act on that. It is a true, correctly calculated, carefully formatted number, and it changes nothing. That is a report, and reports are useful. A dashboard that tells you the servers are up is doing its job. It is just not analysis. Now the same underlying data, worked further. "Monthly-plan accounts that never opened the import tool churn at 9.2%. Ones that did churn at 1.8%. The email introducing that tool goes out on day 14, and most cancellations happen on day 11." Who acts? The lifecycle marketing owner. What do they do? Move the email to day 3. That is analysis, and the only difference is that it ended somewhere a person can stand. The word "analysis" is doing a lot of quiet work in job descriptions, so
AI 资讯
I built a tool that won't let you merge AI-written code until you can explain it
The problem AI agents like Claude Code and Codex write code fast. You run it, it works, you merge. A week later, there's a bug — and you realize you never actually understood the code you shipped. You just transcribed it. This is "vibe coding," and it's becoming the default way a lot of us write software now. What I built BuildIt is a set of hands-on courses where an AI agent proposes code changes like a normal diff — but you can't move to the next step until you explain, in an actual conversation with an AI tutor, why the change was made and what could go wrong. You also write the prompt yourself before the AI generates anything. No skipping. No checkbox you can fake. Real, compilable code from lesson one — not toy examples. 9 courses, 45 real shipped projects: Arduino STM32 (HAL) STM32 (LL) ESP32 Next.js Python React React Native Flutter How it works An AI agent proposes code (same diff screen you already know from Claude Code, Codex, Antigravity) BuildIt demands a line-by-line explanation before you can approve it An AI tutor verifies your understanding through real conversation Only then do you move to the next step Technical details The tutor AI runs entirely locally in your browser — your code never leaves your machine Credits-based pricing — unlock a course, it's yours even if you cancel later Built for teams too — share credits across an org, instill review habits from day one Why this matters AI will write more of our code over time, not less. That makes the ability to actually read and verify it more valuable, not less. BuildIt isn't trying to teach you to write code from scratch — it's trying to make sure you don't lose control of the code an AI writes for you. Would love feedback from anyone who's felt that "I merged this AI diff and don't actually understand it" moment. Try it here
AI 资讯
Part 7: Iterating to Green: Real Bugs, and When You'd Actually Reach for a Framework
Part 7 (final) of a series building a support-ticket agent with no framework. Previous: Part 6 (observability). Repo: github.com/akash-pal/agent-from-scratch The other six parts described the finished design. This one is about what "finished" actually took — the real bugs the eval set caught, and the two questions every agent build eventually has to answer honestly: do you need more than one agent, and do you need a framework. Full detail on everything below: docs/iteration-log.md . The iteration log, condensed 1. Exact trajectory matching was the wrong check. First eval run: 12/21 passed. Most failures were the agent correctly sending a confirmation email where the eval only expected a lookup — correct behavior, wrong assertion. Fix: switched the harness from exact-array equality to ordered-subsequence matching (every expected tool must appear, in order; extra steps in between are fine). Still catches a missing, reordered, or wrong tool. Stops false-failing on benign non-determinism. 2. No retry/backoff meant a transient error crashed the whole run. A 503 — model overloaded on case 1 took down the entire eval harness. Fixed with exponential backoff on 429 / 503 specifically, plus inter-case pacing to stay under free-tier rate limits. 3. A -latest model alias silently rolled onto a stricter quota. gemini-flash-latest worked, then started failing with a 20 requests/day cap after quietly resolving to a newer model. Fixed by pinning an explicit model version instead of an alias, after checking the provider's live usage dashboard for actual quota — 25x more headroom on the pinned model. The takeaway generalizes past this one provider: "latest" aliases optimize for capability, not quota stability, and what they resolve to changes over time without your code changing at all. 4. A testing artifact that looked like a real bug. Piping multiple answers into the interactive CLI via printf "a\nb\n" | npm run agent intermittently hung after the first prompt. Root cause: a Node.j
AI 资讯
Part 4: The Raw ReAct Loop: ~100 Lines, No Framework
Part 4 of a series building a support-ticket agent with no framework. Previous: Part 3 (the eval set). Repo: github.com/akash-pal/agent-from-scratch This is the part everyone reaches for a framework to skip. Here's the argument for not doing that, at least the first time: if you can't explain what your agent loop does in plain English, no framework is going to fix that — it's just going to make the loop harder to see. Here's src/agent.ts , trimmed to the actual loop: export async function runAgent ( ticket : Ticket , customer : Customer | null , approvalFn : ApprovalFn , maxSteps = 8 , ): Promise < AgentResult > { const state = initState ( ticket , customer ); // Guardrail check happens BEFORE any model call — see Part 5. const escalatePattern = matchAutoEscalate ( ` ${ ticket . subject } ${ ticket . body } ` ); if ( escalatePattern ) { return { outcome : " escalated " , finalText : `ESCALATED: auto-escalated — " ${ escalatePattern } "` , state }; } const ai = new GoogleGenAI ({ apiKey : process . env . GEMINI_API_KEY }); const contents : Content [] = [{ role : " user " , parts : [{ text : ticketToUserMessage ( ticket ) }] }]; for ( let step = 0 ; step < maxSteps ; step ++ ) { const response = await withRetry (() => ai . models . generateContent ({ model : MODEL , contents , config : { systemInstruction : buildSystemPrompt ( COMPANY ), tools : [{ functionDeclarations }] }, }), ); const calls = response . functionCalls ?? []; if ( calls . length === 0 ) { // No tool call — the model produced a final answer. Done. const text = ( response . text ?? "" ). trim (); return { ... enforceOutcomeIntegrity ( parseOutcome ( text ), state ), state }; } // Otherwise: execute the requested tool(s), feed results back, loop again. contents . push ({ role : " model " , parts : response . candidates ?.[ 0 ]?. content ?. parts ?? [] }); const responseParts = []; for ( const call of calls ) { const result = await executeToolWithGuardrails ( call , state , approvalFn ); // Part 5 respon
AI 资讯
Part 2: Pinning the Use Case and Writing Tool Contracts Like Specs
Part 2 of a series building a support-ticket agent with no framework. Part 1 covered why. This part covers Steps 1–2 of the build order: pinning the use case, and writing tool contracts. Repo: github.com/akash-pal/agent-from-scratch Before any code, two documents: docs/use-case.md and docs/tool-contracts.md . Skipping this step is the single most common reason teams end up with an agent nobody trusts — not because the idea was bad, but because nothing downstream (evals, prompts, memory) had a fixed target to hit. Step 1: pin the use case Four gates, filled in before writing a line of code: Gate Definition Bounded input One support ticket: { subject, body, customer_id, order_id? } Bounded output Exactly one of: resolved , refund_proposed (pending approval), escalated (with a reason) Tool count 5 Success metric Resolution rate > 85% without escalation; escalation rate < 10% The tool count cap matters more than it looks. An agent given 10+ tools starts hallucinating tool names and picking the wrong one — a cognitive load problem, not a dependency problem. Keeping this agent to 5 tools, covering exactly three request types (order status, refunds, KB lookups), keeps every run in the healthy 3–8 tool-call range instead of ballooning into a system that needs to be split into multiple specialist agents. (Part 7 covers the actual cost math for when a split is worth it.) Bounded output matters too: resolved / refund_proposed / escalated isn't just documentation — it becomes a literal parseable prefix ( RESOLVED: , REFUND_PROPOSED: , ESCALATED: ) that the agent's final message must start with. Part 4 shows exactly how that gets parsed, and Part 5 shows why trusting that string alone turned out to be a real bug. Step 2: tool contracts are a schema, not a docstring This is the part that's easy to under-invest in. A tool's description field isn't a comment for future developers — it's the only thing the LLM reads to decide when to call the tool. Treat it as a specification. Here'
AI 资讯
How to audit a free AI visibility score with six manual checks
A free AI visibility score is auditable only when you can inspect the prompt, engine, raw answer, date, and denominator. Treat the score as a test result, not a property of your brand. This tutorial builds a six-check control you can run by hand, store as plain data, and compare with any tool's output. The workflow takes three buyer questions, runs them in two AI surfaces, and records the six answers without trying to force agreement. It will not estimate your entire market. It will tell you whether a dashboard's headline number has enough evidence to be investigated. What does an AI visibility score measure? An AI visibility score usually summarizes brand presence across a defined set of generated answers. That definition contains the trap: the question set is part of the metric. So are the engine panel, run date, session state, retrieval mode, and rule used to count a “hit.” Remove those inputs and the number is not reproducible. Imagine a tool asks three questions in two engines. That creates six cells. If your brand appears in two cells, the simple presence result is: presence = brand_present_cells / total_cells presence = 2 / 6 presence = 0.333... = 33.3% The arithmetic is trivial. The evidence is not. A different tool can ask five different questions in three engines and produce a different score without contradicting the first run. The two tools measured different grids. Keep the unit explicit: “present in two of six generated answers on this date” is defensible. “Our AI visibility is 33” is incomplete. Which evidence fields should you require? Require five fields for every result: prompt, engine, raw answer, timestamp, and counting rule. Use a sixth field for cited sources when the surface exposes them. A source-only appearance and a prose mention can signal different problems, so do not merge them silently. Here is one real saved result from Webappski's public 14 June 2026 tracker report: { "run_date" : "2026-06-14" , "prompt" : "beste Answer Engine Optimiz
开发者
Why a live payment is not a release test
Why a live payment is not a release test The riskiest way to test a SaaS checkout is to make a real payment to yourself. It feels reassuring: the live checkout opened, the card worked, the webhook fired and the refund came back. But that proof mixes engineering QA with revenue evidence. Three different proofs A cleaner billing release process separates three questions: Does billing behave correctly? Test payment, refund, webhook and subscription edge cases in a Stripe sandbox. Is production configured correctly? Verify the live price, currency, checkout destination, webhook configuration and deployed revision without moving money. Did a customer pay? Treat a genuine live transaction as customer activity and revenue evidence, not as an engineering fixture. Stripe documents sandboxes as isolated testing environments and separates sandbox credentials from live credentials. The practical lesson is broader than Stripe: operational proof and commercial proof should not share the same transaction. A useful boundary Use this sequence: Sandbox QA → read-only production verification → genuine customer payment . It keeps release evidence, reconciliation and revenue numbers easier to interpret. We recently tightened the same boundary in VendorOS. That does not prove live customer revenue; it is a workflow lesson about keeping evidence categories separate. If your release process still requires a live self-payment, ask which part of the verification can become read-only. Sources: Stripe Sandboxes Stripe API keys Stripe testing VendorOS release boundary
AI 资讯
Build a JSON-RPC 2.0 API in Symfony in 15 minutes: from composer require to OpenAPI
REST works great while your API describes resources. But as soon as the domain becomes verb-shaped - recalculateInvoice , mergeAccounts , assignTask - you end up bending verbs into nouns and arguing about which HTTP method cancels an order. JSON-RPC 2.0 cuts through all of that: every call is just method + params , one endpoint, a spec that fits on two pages, and batching out of the box. In this article we will build a working JSON-RPC 2.0 API on Symfony: a task tracker with DTO validation, batch requests and generated OpenAPI documentation. There is surprisingly little code to write: methods are declared with attributes, validation is derived from property types, and Swagger is generated by a console command. Everything below lives as a ready-to-run project on GitHub: symfony-jsonrpc-api-demo - clone it and poke it with curl while you read. We will use the otezvikentiy/json-rpc-api bundle (PHP 8.2-8.5, Symfony 6.4/7/8; this article uses PHP 8.4 and Symfony 7.4). Full disclosure: I am the author of the bundle. It has been running in production for three years - internal fintech tooling, an HRM system - nothing glamorous load-wise, but the correctness, logging and audit requirements were real, and they shaped most of what you will see below. Installation composer create-project symfony/skeleton: "7.4.*" tasks-api cd tasks-api composer require otezvikentiy/json-rpc-api If Flex has contrib recipes enabled, the bundle registers itself. If not, it is two lines by hand: // config/bundles.php return [ // ... OV\JsonRPCAPIBundle\OVJsonRPCAPIBundle :: class => [ 'all' => true ], ]; Wire up the route and a minimal config: # config/routes/ov_json_rpc_api.yaml ov_json_rpc_api : resource : ' @OVJsonRPCAPIBundle/config/routes/routes.yaml' # config/packages/ov_json_rpc_api.yaml ov_json_rpc_api : access_control_allow_origin_list : - ' http://localhost:8000' The bundle registers a single route, /api/v{version} - every request goes through it. Note the CORS list format: these are ful