HOUHUIYANG.COM

Engineering Notes

AI, Engineering Systems, and Organizational Evolution

Hou Huiyang41 articles

Contents

  1. 01From HTML Timelines to Verifiable Renders: How I Build HHY Videos with HyperFrames
  2. 02From v1.7.0 to v1.7.1: Giving Every HHY Commit a Clear Boundary
  3. 03Building HHY CMS: Turning Website Delivery into a Reusable Product
  4. 04From v1.5 to v1.7: Building a Verifiable Optimization Pipeline for HHY
  5. 05HHY Database 1.0: Building Database Access for Long-Running Applications
  6. 06GPT, Down to the Scalar: How I Read MicroGPT
  7. 07From Scripts to a Resident Service: How I Built the HHY Web Runtime
  8. 08Travel Is Not a Checklist: How the Road Keeps Revising My Map of the World
  9. 09Beyond the Model Race: How I Read the Cross-Pacific AI Chessboard
  10. 10From AST to Bytecode: Making HHY's Default Engine 2.7× Faster
  11. 11Entanglement, Black Holes, and Qi: Three Conjectures About the Universe and Us
  12. 12Do Not Build Only One Career: Opportunity Windows, Second Curves, and Continuous Evolution
  13. 13Building a Multi-API Data Collector: Three Sources, One Incremental Table
  14. 14Building Asset Governance: Audit Before Effects
  15. 15Building DataFlow ETL: CSV, JSON, and HTTP in One HHY Flow
  16. 16Building FlowGuard: A CI-Ready Quality Gate in HHY
  17. 17Building HHY Collector Framework Without Starting from a Scrapy Clone
  18. 18Optimizing HHY's AST Interpreter Without Switching to Bytecode
  19. 19Researching Hong Kong Film Companies with HHY and MediaWiki
  20. 20Building HHY Language: From Flow Philosophy to a C Runtime
  21. 21Why I Put OCR Before the LLM: Image Understanding Is More Than Uploading a File
  22. 22The Next AI R&D Organization: From Functional Roles to Project Cells
  23. 23Software Is Becoming Disposable: How AI Rewrites SaaS and Internal Systems
  24. 24One Content Source, Three Reading Experiences: Web, WeChat Mini Program, and macOS
  25. 25A Vertical AI Model Is a System: Building Atlas, a Local AI Career Assistant
  26. 26Building CoreX: A Native Real-Time macOS Monitor in Swift
  27. 27FaceFizz: Building a Privacy-First Camera Inside the Browser
  28. 28A Packaging Bug I Found in Homebrew Redis 8.10.0
  29. 29Building a Recruitment LLM from Zero to One: A BOSS Zhipin–Style Case Study
  30. 30The Moat in Large Language Models Is Not Just Code—It Is Data
  31. 31Agent Architecture for SMBs: A 90-Day Path to Production
  32. 32Building Docket with Pragmatic DDD
  33. 33Xinxu Cloud Brain AI: Building a Reliable Multimodal Emotion System
  34. 34Modern Concurrency: From Execution Models to Production Reliability
  35. 35Do Large Language Models Understand? From Next-Token Prediction to the Boundary of Intelligence
  36. 36LangChain vs. LlamaIndex: Choosing a Framework and Building Production RAG
  37. 37How to Build an AI-Native Team
  38. 38From Query Understanding to Knowledge-Driven Recommendations
  39. 39My AI Programming Workbench: Orchestrating Models Instead of Hunting for One Best Tool
  40. 40Building Production Intelligent Risk Control: Streaming, Lakehouse, Rules, and Models
  41. 41From Random Weights to Local Inference

01 / 41

From HTML Timelines to Verifiable Renders: How I Build HHY Videos with HyperFrames

A retrospective on using HyperFrames for HHY launch films, tutorials, courses, and vertical shorts: defining evidence first, then organizing scenes, sound, real assets, and automated checks into a repeatable production workflow.
Sep 15, 2026 · 14 min read

Over the past few weeks, I have used HyperFrames to make several very different kinds of video: an HHY Web Runtime launch film, an HHY v1.7.0 product launch, a roughly two-minute getting-started tutorial, a repeatable course pipeline, and a 9:16 mobile short.

Their purposes differ. A launch film needs to establish one memorable idea in a few seconds. A tutorial must keep code and narration aligned. A course must support repeated production. A vertical short depends more heavily on one coherent visual concept. After completing these projects, however, I became increasingly certain of one thing: the difficult part of video production is not making an element move. It is making content, time, media, sound, and verification behave as one system.

HyperFrames is valuable to me because it represents video as HTML. Scenes are DOM structures, animation has addressable time, images, code, audio, and sub-compositions are project files, and a deterministic render pipeline produces the final MP4. It feels familiar as an engineering medium: visual work still requires judgment, but the structure is readable, changes are reproducible, and errors can be checked.

The HHY v1.7.0 launch-film brand lockup

I Define What the Video Must Prove First

The easiest way to lose direction is to begin with transitions, particles, or a 3D effect. The frame may become busy while leaving the viewer unable to explain what the product is.

I now start with a short production contract that answers at least five questions:

QuestionAnswer I need
Core messageWhat single sentence should the viewer remember?
AudienceDevelopers, learners, or a broader mobile audience?
EvidenceWhich code, interfaces, and results can I show truthfully?
BoundariesWhich capabilities are not shipped and must stay out?
DestinationLandscape, portrait, website, social media, or a course?

For the HHY v1.7.0 launch film, the throughline was not “we built many features.” It was: HHY has grown from a flow-first language into a system for building real applications. I divided 41 seconds into six responsibilities: Logo, language, compiler, ETL, CMS, and invitation. Each section carries one information task.

This also controls technical claims. A release film can say “Verified optimizing compiler and Runtime,” but a faster result in one bounded benchmark must not become a performance promise for every program. Short video copy tends to remove conditions, so evidence boundaries must be settled before animation begins.

A Storyboard Is a Time Budget, Not a Shot List

I once treated a storyboard as a list: show a title in scene one, code in scene two. In production, it behaves more like a time budget.

A six-second scene cannot comfortably contain a title entrance, three lines of typed code, an unfolding architecture, a result count, and an exit transition. The software can execute all of those events, but the viewer cannot finish reading them.

I break a scene into smaller phases:

0.0–1.4s  establish the headline
1.4–3.2s  show inputs and direction
3.2–4.8s  reveal the central model or result
4.8–6.0s  hold long enough to read

That final hold matters. Animation tools can make stillness feel like wasted time. If every object keeps moving, however, the viewer sees change rather than a conclusion.

In a launch film, I usually assign one primary action to each scene. The |> operator traveling from depth and flipping into the HHY Logo is the opening action; the version line and URL only need to arrive clearly. Turning every detail into the hero removes the actual memory anchor.

HTML Lets Me Treat a Frame as Maintainable Structure

A HyperFrames composition is not a recording of browser activity. The renderable page itself is the video source. The DOM declares when an element begins, how long it lasts, and where it belongs. A paused, seekable timeline means that the same timestamp should produce the same frame.

That creates several practical benefits:

  • copy remains editable and localizable instead of being flattened into an image;
  • code, terminals, and data cards can remain real text with stable layout;
  • scenes can become sub-compositions rather than one endlessly growing HTML file;
  • landscape and portrait pieces can share a design language without sharing one composition;
  • screenshots at fixed timestamps become review evidence instead of relying on “it looked fine from the beginning.”

HTML does not automatically produce good design. A web page lets a person scroll, wait, and click; a video advances on a fixed clock. A card layout that works on a site can look fragmented at 1920×1080. A beautiful long page can become unreadable when reduced inside a video frame.

My working rule is to use large type, short copy, an explicit hierarchy, and a limited amount of simultaneous information. Real product screenshots must be large enough to inspect. Code should show only the part being explained. Background and decoration must not compete with the body copy for contrast.

Architecture communication in the HHY Web Runtime launch film

Real Assets Decide Whether the Video Is Credible

I prefer the project's actual Logo, executable code, real commands, and real interfaces. A model-generated or illustrated image can establish atmosphere, but it cannot replace product evidence.

The CMS scenes in the HHY v1.7.0 film use the real website and administration interface. Customer counts, departments, and output in the ETL sequence come from a verified fixture. The getting-started tutorial uses runnable .hhy files and real commands, not temporary syntax written solely for the frame.

One practical lesson has become a rule: validate the asset before designing animation around it. If I discover at the end that a screenshot has the wrong aspect ratio, a command fails, or data does not match the release, composition and pacing both need rework.

Once an asset enters the project, I try to freeze it locally. A render should not depend on a network request, randomly select an image at runtime, or change because a remote file changed. This is both a reliability requirement and a provenance requirement.

Tutorial Timelines Must Follow the Learning Process

A product film can use pace to create emotion. A tutorial cannot optimize only for speed. The viewer needs to see the relationship among concept, source, execution, and result.

For the HHY getting-started video, I divided the material into six sub-compositions: welcome, installation, bindings, functions, Flow, and run. Each has its own narration and frame design; together they form one root timeline.

Functions and pipe syntax in the HHY getting-started tutorial

The process left me with several lessons:

  1. Show the outcome of a section before explaining its syntax.
  2. Code reveal speed should follow narration, not an animation preset.
  3. Commands, source, and output need distinct visual levels.
  4. Captions must not cover the code currently being explained.
  5. Long sentences should be split using measured speech duration rather than equal timing.

Course production amplifies these concerns. One video can be adjusted by hand. If every episode in a 16-lesson course starts from a blank canvas, visual language and quality drift quickly. I separated the course pipeline into scripts, runnable exercises, narration, sentence timing, captions, compositions, rendering, media validation, and website synchronization.

A template should not force every lesson to look identical. It should settle the parts that should not be debated repeatedly: resolution, brand colors, title area, code area, caption safe area, fonts, audio format, and output checks. Learning objective, example, and pacing remain specific to each lesson.

Sound Is Not a Track to Paste on at the End

For an unnarrated launch film, I establish visual beats first and let music and a few effects support the transitions. In a narrated tutorial, the dependency reverses: measured speech duration determines scene length, and the frame must enter and hold around each sentence.

If I finish an imagined 60-second animation and later receive 73 seconds of narration, increasing playback speed rarely solves the problem. Captions, code reveals, reading holds, and scene connections all need to be redistributed.

I now treat speech as timing data: generate or record it first, measure each sentence, and build scenes and captions from those durations. During rendering, visual video is muted and sound is managed on separate tracks, making replacement and verification easier.

Sound effects require restraint too. A keyboard confirmation, program completion, or major scene transition can justify one distinct cue. Giving every word entrance a sound rapidly consumes attention. Sound should clarify an event, not demonstrate how many effects the tool can play.

One Design Language Does Not Mean One Aspect Ratio

A landscape launch film can use horizontal relationships and wide negative space. A vertical short is closer to single-column storytelling; its subject must remain inside a narrow mobile viewing region. Simply cropping a 16:9 composition to 9:16 usually loses both copy and subject.

For the HHY vertical short, I used one continuous spatial idea: a person lives inside a macOS desktop, Terminal rises above the Dock, a real command is typed, and the HHY Logo emerges from the successful result. The 22-second piece uses no cuts. Every change preserves the same spatial relationship.

This project reminded me that creativity does not necessarily mean more shots. One understandable spatial rule with staged reveals can feel more complete than a sequence of unrelated backgrounds. Portrait work particularly needs deliberate safe areas among the menu bar, Terminal, person, Logo, and closing copy.

Fixed-Time Checks Are More Reliable Than One Playback

The easiest video defects to miss often last only a few frames: copy overlaps during an entrance, an outgoing element remains visible, a transition exposes empty space, an image briefly changes aspect ratio, or a line of code clips mid-animation.

I keep three validation layers:

LayerWhat it checks
Static rulesTiming attributes, asset paths, determinism, composition structure
Runtime and layoutPage errors, overflow, occlusion, and contrast
Render evidenceTimestamped snapshots, contact sheets, media metadata, and final MP4

Watching the full piece remains necessary, but it is best for judging rhythm and emotional continuity. Fixed-time sampling is more efficient for layout problems. I capture launch-film posters when the scene's message is stable and inspect both sides of transitions. In tutorials, I additionally compare caption, code reveal, and speech at the same timestamp.

Automated checks do not replace visual judgment. A passing check means the known rules found no error. It does not mean the story is clear, the type is ideal, or the pacing cannot improve. I want the tools to find computable defects while a person decides whether the information deserves to appear and whether the viewer has enough time to understand it.

The Workflow I Kept

After these projects, my working sequence looks like this:

one core message
→ content and evidence boundaries
→ storyboard and time budget
→ prepare and validate real assets
→ HTML scenes and a seekable timeline
→ align voice, captions, and visuals
→ timestamped snapshots and automated checks
→ full-playback review
→ render and validate the final file

The durable lessons are not transition parameters. They are three principles.

First, decide what the viewer should understand before deciding how the frame moves.

Second, put real content into the video instead of using visual polish as a substitute for evidence.

Third, make the video reproducible, inspectable, and editable. When a version, command, image, or language changes, I want to edit structured source rather than reopen a project that can only be adjusted from memory.

HyperFrames does not remove judgment from video creation. It places that judgment inside a medium that feels natural to an engineer: HTML, timelines, files, rules, and repeatable rendering. To me, this is not turning a video into a webpage. It is turning a one-off production process into a content system that can continue to evolve.

A well-engineered video is not the one with the most animation. It is the one where message, evidence, rhythm, and frame agree on the same timeline.

02 / 41

From v1.7.0 to v1.7.1: Giving Every HHY Commit a Clear Boundary

From the optimizing compiler back to resource ownership, failure recovery and controlled concurrency: my engineering decisions in HHY v1.7.1, its measured baseline, and the path toward shared state, coroutines and distributed transactions.
Sep 14, 2026 · 14 min read

After shipping the optimizing compiler in v1.7.0, I returned to questions that sound less advanced: who keeps an environment alive, what a closed Stream still retains, who collects child processes after a task fails, and whether an update has actually committed when the caller receives an error.

These questions are less visible than new syntax. They determine whether I can trust HHY to keep running and to modify real application state.

My previous article followed the path from resource lifetimes to HIR/MIR. v1.7.1 continues that work by repairing ownership, defining several kinds of commit, and establishing a performance baseline with the current binary.

The most important progress, for me, is giving success, failure and an unknown outcome distinct meanings. Shared state, coroutines and distributed transactions will all need that foundation.

This article draws on the September 14, 2026 language report and archived release evidence. The performance table uses that report's measurements; I did not rerun benchmarks while writing. Future directions follow the public roadmap's 1.x, 2.x and 3.x series as of the same date. They are plans, with no calendar commitment.

From the v1.7.0 optimization pipeline to v1.7.1 ownership and commit boundaries

What v1.7.0 established, and what v1.7.1 takes on

v1.7.0 shipped structured HIR, six static optimization passes, bounded integer MIR, type-feedback guards and deoptimization, and scalar replacement for eligible local integer Lists. That pipeline moves some repeated work into compilation and subjects the result to an independent Verifier.

Shipping it does not mean every application becomes faster by default. Direct Bytecode remains the default, with AST as an independent semantic reference. HIR, feedback specialization and scalar replacement remain opt-in. Scalar replacement also preserves the original GC and quota reservations; it does not establish that heap allocation has been eliminated.

For v1.7.1, I focused on the layer closer to applications. HHY now has files, HTTP, processes, Streams, a persistent Web Runtime and a database extension. A call can lend out a cursor. A lazy pipeline can retain a closure environment. A failed parent task can leave behind external writes that already succeeded.

The Runtime needs to explain those lifetimes before more fast paths make them harder to follow.

Repairing resource ownership first

Explicit GC roots for Context environments are an important part of this release. Environments needed by repeated host calls must remain alive through collection. Candidate state, transactions and strict scopes also participate in root management and exceptional-exit cleanup.

Closing a Stream now releases payload, environment, callback and jobs references. Registry chain nodes remain to protect checkpoints. Describing this as immediately deleting every resource registration would hide the actual lifetime design.

Task child-process failure recovery and the portable no-replace fixes address the same responsibility: every state established on the successful path needs an owner when execution exits early.

Memory accounting also needs a precise description. max_memory limits the language heap relative to the Runtime startup baseline. It is neither process RSS nor total memory across all Workers. To assess a persistent service, I still need RSS, file-descriptor, child-process and temporary-file observations over time.

Seven experimental APIs, four responsibilities

The seven controlled-concurrency APIs in v1.7.1 are disabled by default:

HHY_CONCURRENCY_EXPERIMENTS=1 hhy run app.hhy

They cover state publication inside a Runtime, cooperative updates to one file, strict database transactions and supervision of finite task batches.

ScopeAPIsResponsibility
Single-owner stateatomic_state, atomic_read, atomic_update, atomic_closeValidate a candidate and publish the complete value once
One fileatomic_file_updateRead, modify and replace under one cooperative lock protocol
Databasetransaction_strictBind constrained callback operations to one transaction; prevent commit after failure
Bounded taskstask_mapIsolated child processes, ordered results, failure supervision and direct-child cleanup

These interfaces do not make HHY a shared-memory concurrency language. Its current model combines a synchronous Runtime, isolated processes, bounded parallelism and external storage transactions. There is no new coroutine scheduler or async/await.

I want each guarantee to start with a resource it can actually govern. Atomicity becomes useful when its scope is explicit.

Suppose two fields need to change together:

let state = atomic_state({left: 100, right: 0})
fn transfer(old) { {left: old.left - 10, right: old.right + 10} }
atomic_update(state, transfer)
print(atomic_read(state))
atomic_close(state)

atomic_update synchronously computes and validates the candidate, then publishes it once. Errors, cancellation and quota failure before commit leave the old value intact.

The owner is the originating Runtime/PID. AtomicState cannot be sent across Workers, captured into another Worker or JSON-encoded. Ordinary variables have not become thread-safe, and cross-process shared memory has not been implemented by this API.

Candidate data is constrained to Null, Bool, Int, finite Float, String and recursive Lists or Maps. Functions, Streams and handles are excluded. That restriction gives publication and lifetime checks a manageable scope.

This is a small commit kernel I can validate. Multiple objects and multiple Workers will require a new memory model and recovery protocol, rather than a broader claim about the existing handle.

Files, databases and tasks do not form an automatic transaction

Three independent atomic domains and a separate bounded-task supervision scope

File updates are easy to misunderstand. Atomic replacement controls visibility. Avoiding lost updates when several writers read and modify a file also requires a common cooperative lock protocol.

atomic_file_update requires an existing regular file. Writers must cooperate through the same path and stable .hhy-lock file. Its Duration limits lock waiting only; it does not interrupt an arbitrarily long callback. Deleting the lock file while writers are active can change the identity of the lock they rely on.

Publication and durability remain separate. Writing and syncing a temporary file, replacing the target and syncing its parent directory each have failure points. HHY_PUBLISHED_DURABILITY_UNKNOWN means publication has occurred but directory durability is uncertain. Treating that error as proof that nothing was written can make a blind retry repeat a business action.

The database has a related problem. transaction_strict binds callback work to one transaction. A runtime error in that callback leaves it rollback-only even if the error is caught; swallowing the error cannot restore permission to commit. Strict callbacks cannot perform arbitrary file, HTTP or task effects either.

When a COMMIT response is lost, however, the client may receive DB_COMMIT_UNKNOWN. The database can have committed without the client receiving confirmation. Applications need a unique request key, result lookup and reconciliation before deciding what to do next. The language does not automatically provide exactly-once execution.

task_map takes a finite List, supervises bounded isolated tasks and returns results in input order. A later task's fast failure can be noticed without waiting for the first slow task to finish. On failure, direct children are terminated and collected together. External file, HTTP or database effects already completed by those children are not undone. Its result budget is not a global disk or RSS limit either.

I want callers to know which changes commit together and which recovery steps remain theirs. Runtime state, one file and a database form three independent atomic domains. Task supervision manages lifetimes. They do not automatically compose into a cross-resource transaction.

The baseline complicates the phrase “faster by default”

The September 14 report runs three existing benchmarks with both AST and Bytecode using the current 1.7.1 binary. Each mode has two warmups and seven measured samples, with alternating engine order, HHY_* experiment variables cleared, and matching output checked for every run.

Each sample starts a fresh process. Timing includes startup, parsing, compilation or verification, and execution. These are end-to-end measurements, rather than steady-state hot-loop timings.

HHY 1.7.1 Bytecode-to-AST time ratios: faster Flow, similar JSON, slower captured closures

WorkloadAST medianBytecode medianBC / AST
core-flow, 100,000 items20.922 ms13.499 ms0.645×
json-flow, 500 conversions62.382 ms63.469 ms1.017×
call-closure, 100,000 captured-closure calls21.985 ms28.518 ms1.297×

Bytecode takes about 35.5% less time on Flow, roughly similar time on JSON, and 29.7% more time on captured closures. All three produce equivalent output; their performance does not move in one direction.

I want to keep the closure row visible. The next useful step is to separate startup and steady-state cost with profiler, dispatch and lookup comparisons. This table alone cannot identify the specific lookup or dispatch mechanism responsible for the difference.

It is also not a v1.7.0 versus v1.7.1 comparison. It compares two engines in the same 1.7.1 binary. The development machine was not frequency-locked or CPU-pinned, background services remained active, and there were no multi-host repeats. The data gives me a starting point for investigation, not p99 latency, a production SLA or a ranking against other languages.

Development-time binding-cache and source-fusion records also show mixed results: small benefits in some workloads and regressions in workloads such as file distinct. Their comparison binary was saved before development rather than rebuilt cleanly from a tag. Dividing those absolute timings by this new table would not establish a release-to-release speedup.

Compiler, cache and fusion candidates therefore retain independent admission decisions based on real workloads, compatibility and resource budgets. An implementation alone is insufficient reason to enable one by default.

Keeping the unanswered questions beside the evidence

The report newly ran 69 engine/configuration cases and 80 contending cooperative file updates. The 69 cases include expected rejections: passing means behavior matches the contract, not that every input succeeds.

Archived evidence also includes database idempotency and unknown-commit recovery, a 100,000-request Web test, four-platform CI and release-package validation. The Web record reports 32 clients, no failures and roughly 2,391 requests per second. That is an observation from one machine and workload, not maximum capacity or database application throughput.

The 120-second soak with 9,360,000 owner commits belongs to an older development binary. It is neither a 24-hour soak of the current release nor a shared-memory concurrency test. Linux x86_64 also has a BDWGC/ASan limitation in the extremely low-memory unwind path; sanitizer coverage should not be described as identical on every platform.

Release measurement had visible variability too. The first macOS I/O/JSON ratio was 1.3496, above the 1.10 gate. A local check of the same commit returned 1.0032; rerunning the failed jobs passed without changing code or thresholds. Retaining that history is more useful than saying everything passed on the first attempt.

My next priorities are a 24-hour soak on the candidate build, faults before and after publication, and database invariants across isolation levels, deadlocks, duplicate requests and restart recovery. Web concurrency steps, tail latency and resource curves follow. Claims should expand with the evidence.

1.x: Stabilize existing APIs individually

The public roadmap groups the next work into three major series. I use that level here rather than turning every patch number in an internal planning draft into a promised release.

For 1.x, the focus is the existing seven APIs: fix ownership, effects, error categories and resource contracts, then validate the candidate with soak testing, fault injection, platform coverage and AST/Bytecode parity.

State, file, database and task capabilities should be judged separately. A mature capability can lose its experimental gate while another stays experimental.

Stable availability means an admitted API can be called without HHY_CONCURRENCY_EXPERIMENTS. Developers still explicitly request atomic updates or strict transactions. An upgrade will not silently turn ordinary variables into transactional state.

Performance optimization has a separate gate. HIR, caches or fusion still need end-to-end benefits after correctness passes, and may remain disabled indefinitely.

2.x: Design shared state and coroutines together

Future HHY series: 1.x stabilization, 2.x shared atomic state and coroutines, 3.x distributed transactions

The 2.x goals are domain-wide state atomicity, atomic shared memory across Workers and a coroutine scheduler.

“Global” has a boundary here: managed shared objects inside a declared StateDomain participate in unified multi-object transactions. Arbitrary ordinary variables, files, HTTP calls and databases do not automatically join that transaction.

Shared memory cannot simply contain pointers into today's GC heap. It needs a shared arena, generation-checked handles, versions and read/write sets, a unified commit point, and recovery and reclamation rules after Worker crashes. Sharing among Workers on one host is distinct from consistency across nodes.

Coroutines require resumable execution frames that retain local values, exception handlers, resource scopes and GC roots, followed by nonblocking I/O and structured cancellation. Existing Stream or SSE support does not mean this kernel exists already.

I am particularly interested in the intersection: whether execution may suspend during atomic publication, who cleans a candidate when cancellation arrives, whether a waiting task can lose its wakeup, and who retains old versions. Independently tested components still require joint validation.

The new mode should begin experimentally and become stable after correctness, recovery, fairness and resource acceptance. Existing programs must retain their sharing and scheduling semantics unless their authors choose to migrate.

3.x: Make uncertain outcomes recoverable across nodes

The 3.x direction is distributed transactions: a durable coordinator, prepare/commit/rollback participants, decision logs, UNKNOWN queries and recovery, followed by high availability, fault injection and operational tooling.

I want to support participants with an explicit transaction protocol. An ordinary HTTP service without recoverable prepare and commit does not acquire atomic rollback merely by being called from HHY. Explicit compensation is useful, but it has a different guarantee.

Acceptance will focus on participant crashes, network partitions, duplicate messages, lost acknowledgements and how a restarted coordinator continues a recorded decision. Stable APIs will still require explicit databases, coordinator configuration and a supported-participant matrix.

From v1.7.0 to v1.7.1, my aim for HHY remains clear data flow and explainable system behavior. With every capability I add, I now ask one more question: after it fails, can the caller still determine what happened and finish the work?

03 / 41

Building HHY CMS: Turning Website Delivery into a Reusable Product

An engineering account of HHY CMS 0.1.2: HHY Web and MySQL, recoverable installation, draft and publication boundaries, theme separation, and the responsibility of building software for other people.
Sep 08, 2026 · 11 min read

Business websites tend to have familiar requirements: introduce the company, present products and customer stories, publish news, and provide contact information. Yet finishing the pages is only part of delivering the website.

Who updates the content? Can an unfinished edit appear on the public site? Does replacing a cover image require a developer? Can someone recover from an interrupted installation? What must they back up before moving to another server?

These are the questions behind HHY CMS. I am building a product that packages the recurring work of content management, publication, and website delivery, so other people can install it in their own environment and maintain a business website themselves.

This article follows the current 0.1.2 source code. It explains the boundaries I chose and why seemingly small constraints deserve attention in the first release.

Delivering a website means handing over the ability to update it, an understandable set of operating boundaries, and a way to recover when something goes wrong.

Define the smallest complete product

The first release includes a public business website and its administration interface: home, about, products, cases, news, and contact pages, supported by categories, media, site settings, and homepage sections.

That scope shapes the data model. Products, cases, news, and pages share fields for titles, slugs, summaries, bodies, categories, covers, and SEO metadata, with kind distinguishing the content types. Brand information, contact details, navigation, and footer content belong to site settings. Homepage sections have their own ordering, visibility, and copy.

I have kept arbitrary content modeling, freeform layout editing, multiple tenants, and a plugin marketplace outside this release. Every additional degree of freedom needs validation, compatibility rules, and maintenance. A focused product helps reveal which differences between websites deserve to become reusable capabilities.

Reuse therefore has a concrete meaning: companies can change their brand, content, and homepage organization while sharing the same implementation of saving, publishing, authentication, and media management.

Give HHY the full server-side responsibility

HHY CMS runs on HHY Web and MySQL. HHY implements installation, routing, permissions, database access, and server-rendered pages. CSS and JavaScript improve the browser experience. Deploying the CMS itself requires neither Node.js nor a frontend build step.

The current documented environment is HHY 1.5.0, the official database 1.0.0 extension, MySQL 8.x or a compatible version, OpenSSL 3, and the system file utility. An explicit dependency contract is more useful to someone receiving the software than a vague promise of effortless setup.

The code follows business responsibilities:

ModuleResponsibility
lib/application.hhyRoute registration, dispatch, and shared error responses
lib/install.hhyInstallation validation, migration records, initialization, and recovery
lib/auth.hhyLogin, logout, and login rate limiting
lib/content.hhyEditing, saving, publishing, unpublishing, and preview
lib/manage.hhySite settings, homepage sections, categories, and media
lib/site.hhyPublic page data and sitemap generation
themes/default/theme.hhyDefault theme presentation

This makes the system readable through the actions it supports. Publication changes lead to the content module; presentation changes lead to the theme; installation failures lead to the installation state. The same map helps the next developer find their way.

The application entry point registers static resources with HHY Web. This excerpt comes from the implementation:

let mut app = hhyweb.minimal()
    |> hhyweb.static_files("/assets",path("public"))
    |> hhyweb.static_files("/media",c.file("uploads"))

These mappings also describe a deployment boundary. Assets and uploaded images may be public. Private configuration, installation tokens, and temporary files must stay outside those public mappings.

Treat installation as a recoverable workflow

A personal project can often start with manually created tables and a configuration file. Software delivered to someone else needs an installation experience that stands on its own.

HHY CMS generates an installation token on first startup. The wizard collects database, site, and administrator information. The deployer prepares the database in advance, and a fresh installation requires it to be empty. The submitted database endpoint must also appear in the environment's CMS_DB_ALLOW setting.

The form collects configuration; the deployer decides which database endpoints the application may contact.

Installation must also survive partial progress. The implementation writes install-pending.json before migrations and initialization. The cms_migrations table records completed steps, while a database lock prevents concurrent execution of the migration phase. A retry uses the original pending installation details. The final configuration is written after initialization succeeds.

I distinguish resuming the same installation from overwriting an existing site. An installation identity in the database checks ownership, and the installation endpoint closes once the site is installed. Recovery should not quietly become reinstallation.

These mechanisms support the current installation workflow. They are not a complete upgrade system: schema evolution, compatibility with older releases, and upgrade rollback still need their own design.

Separate drafts from the public version in the data

Few behaviors undermine an editor's confidence faster than an unfinished change going live when they click Save.

HHY CMS keeps editable fields and a public_json snapshot in the same content record. Saving a draft updates the editable state. Publishing replaces the snapshot. Public pages read the snapshot, while authenticated preview reads the current editable content.

ActionEditable contentPublic website
Create and save a draftStore the new contentKeep it private
Edit published content and save a draftStore the changesKeep the previous publication
PublishSave content and replace its snapshotShow the new public version
UnpublishKeep the editable contentRemove the public version

This is the publication update in lib/content.hhy:

if f.intent == "publish" {
    let snapshot = put(item,"revision",c.str(revision+1))
    c.exec(tx,"UPDATE cms_content SET public_json=?,public_category_id=NULLIF(?,0),public_cover_id=NULLIF(?,0),published_at=UTC_TIMESTAMP() WHERE id=?",[encode_json(snapshot),item.category_id,item.cover_id,saved_id])
}

The separate public_category_id and public_cover_id matter too. Keeping the old body while allowing category filtering or cover references to follow a new draft would leave the publication boundary incomplete.

This snapshot applies to content records. Site settings and homepage sections have separate save paths, so this is not a site-wide draft system. It also does not provide a complete history of revisions with rollback.

One administrator can still create concurrent edits

The first release supports one administrator. That person can still open two editing tabs and accidentally overwrite a newer change from an older form.

Saving reads the record with SELECT ... FOR UPDATE inside a transaction, then compares the submitted revision with the stored revision. A mismatch produces an explicit conflict message instead of silently overwriting content.

if current != null and (to_int(current.revision) != revision or current.kind != item.kind) {
    throw("内容已被其他操作修改,请重新打开编辑页")
}

The message asks the editor to reopen the page because another operation changed the content. The code excerpt retains the application's original Chinese validation text.

Field validation, category and media existence checks, content updates, publication snapshots, and audit records are organized around the transaction. Failures enter the rollback path; successful saves redirect to the editing page.

Published content also restricts slug changes. This protects existing links within the current feature set. Supporting public URL changes properly would require additional behavior, such as redirects, rather than simply unlocking an input.

Keep publication rules outside the theme

The default theme presents company information as a readable website. site.hhy assembles public data for presentation. The theme does not authenticate administrators or decide when a draft becomes public.

That separation matters because visual identity and information organization vary between companies. Reusing the CMS becomes difficult if every new appearance requires a new permission or publication implementation.

The homepage uses predefined sections with ordering, visibility, and copy controls. Editors get understandable ways to adapt the site while layout combinations remain manageable. There is currently one default theme. The boundary leaves room for future extension, but it is not yet a mature theme ecosystem.

Build editing around everyday actions

Version 0.1.2 adds a searchable, paginated media picker and a visual body editor. The purpose is continuity: an editor should be able to finish a content update without unnecessary detours.

Cover images and logos can be selected or uploaded from the current editing page, with an immediate preview. Selection updates the form; saving makes the association effective. Choosing an image should preserve the text in progress and should not require remembering a media ID.

Media also has a lifecycle beyond the page that first uploaded it. Uploads are limited to PNG and JPEG files of at most 2 MiB, with the system file utility checking the actual type. Deletion checks references from drafts, public versions, and the site logo, protecting images that are still in use.

The body editor uses a local copy of Quill 2.0.3. Its Delta representation is serialized into restricted HTML for headings, bold, italic, lists, and quotations. The server independently applies an allowlist of tags without attributes.

The toolbar therefore does not define the security boundary. Requests that bypass the browser editor still encounter server-side output rules. If the editor fails to load, an HTML textarea remains available, preserving a basic editing path.

The implementation also avoids rewriting untouched legacy content merely because its editing page was opened. Displaying stored content and changing it should be distinct operations.

Delivery quality lives in failure paths

Authentication, MySQL-backed sessions, CSRF checks, parameterized SQL, field limits, login rate limiting, and keeping low-level database errors out of public responses are part of the current implementation. They matter only when applied along actual request paths.

The repository's validation cases cover HTML escaping, rich-text event attributes, invalid slugs, CSRF mismatches, and oversized fields. Editor serialization has separate tests. Navigation newline regression cases capture another practical lesson: something as small as browser form CRLF line endings can become a user's unexplained failure to save.

These checks do not replace complete delivery acceptance. The scenarios I care about include interrupted installation, stale editing forms, draft changes leaking onto the website, and attempts to delete images still referenced by published content. They are directly connected to whether someone can trust the system with their work.

Documentation must explain operating boundaries too. The server listens locally by default; public deployment needs a reverse proxy and HTTPS. Login rate limiting uses the directly connected IP, so shared proxy addresses need consideration. A recoverable backup includes the database, private configuration, and uploaded files. Copying source code alone cannot restore an operating website.

Make each capability understandable and maintainable

HHY CMS remains a deliberately scoped early release: one administrator and one default theme, without multi-tenancy, commerce, multilingual content management, or a plugin marketplace. Publishing this article in two languages does not imply that the CMS already supports multilingual content.

As development continues, I want to prioritize upgrade and recovery paths, validation through more delivery scenarios, and stable boundaries between content and presentation. Before adding a role or a content model, I need to understand how it changes permissions, publication, migrations, and compatibility.

This project also connects HHY Language, the Web Runtime, and Database in a concrete application. A running server and a committed transaction eventually have to support an ordinary person editing a paragraph, saving a draft, and confirming publication.

The HHY CMS I want to deliver is a website product that other people can understand, use, and take over. My work as its developer is to handle the complexity behind those everyday actions.

Further reading

04 / 41

From v1.5 to v1.7: Building a Verifiable Optimization Pipeline for HHY

From database resource lifetimes to VM profiling, calls and unwinding, then HIR/MIR: the engineering behind HHY v1.5.0–v1.7.0, and why shipped optimizations remain opt-in.
Sep 08, 2026 · 16 min read

After building HHY Database 1.0, I returned to the Runtime and Compiler with a more concrete definition of optimization.

Executing fewer instructions is valuable. But once a language supports persistent web services, database transactions, lazy Streams, and closures, speed is one requirement among several. Errors must still occur at the original operation. Cancellation must remain reachable. Borrowed resources must be released. Existing programs must preserve their behavior after an upgrade.

From v1.5.0 to v1.7.0, I followed that problem: integrate application resource lifetimes into the Runtime, identify execution costs, then move provable transformations into the compiler.

The release relationship matters. v1.5.0 was released independently. The v1.6 work comprised VM profiling, experiments, and architecture decisions delivered together in v1.7.0. Version 1.7.0 is released, but the new IR compiler, feedback specialization, and scalar replacement remain opt-in. This is not a story of three successive default speedups.

This article follows release commit 8fb0b8f, the current compiler documentation, and the acceptance policy. Performance figures come from existing final-release measurements; I did not rerun benchmarks while writing it.

HHY v1.5 through v1.7: engineering progression and delivery boundaries

v1.5.0: Resource lifetimes become an optimization constraint

The Database work in v1.5.0 involved much more than SQL. Pools, remote TLS, transactions, cursors, exact types, cancellation, and Worker isolation all required explicit Runtime responsibilities.

A failed transaction callback must not return an uncertain connection to the pool. An incompletely consumed Stream still needs its cursor closed. Extension resources must not outlive a request scope that has ended. Database retains its independent 1.0.0 version; HHY 1.5.0 provides the Runtime integration it needs.

These constraints carry directly into optimization. Changing call frames touches resource unwinding. Changing allocation touches quotas and GC. Deleting code can remove a cancellation check. Reordering expressions can move the first error or an external effect.

The foundation for the next stage was therefore straightforward: Runtime optimizations must preserve the lifetimes that applications already depend on. Once scripts become services, equal printed output is no longer sufficient evidence of equal semantics.

v1.6: Make the VM's costs visible first

The roadmap calls v1.6 VM Architecture & Runtime. I divided that work into four questions without assuming that more instructions or a different collector would be the answer.

StageWork performedOutcome delivered with v1.7.0
v1.6.0Instruction selection and opcode/pair/triple profilingProfiling and evaluation tools shipped; no superinstruction admitted
v1.6.1Call layouts, closures, exception regions, and unwindSwitchable experiments and differential validation; existing capture semantics retained
v1.6.2Binding, call-target, and Map lookup profilingMonomorphic Map slot experiment shipped; disabled by default
v1.6.3GC, memory retention, and scheduling requirementsExisting GC and bounded, ordered parallel retained; no new scheduler introduced

The first challenge is interpreting profiles correctly. HHY_PROFILE_DISPATCH=1 records recursive switch-entry sequences, including opcode and pair/triple counts. These are not necessarily adjacent Bytecode instructions that can be fused. They do not measure pure dispatch CPU time either.

A frequent combination is a candidate for investigation. Turning it into a superinstruction requires proof about evaluation order, error locations, resource checks, and actual execution benefit. Not admitting a superinstruction was a legitimate result of the evaluation.

For me, richer instruction selection means making execution choices explainable through evidence. Increasing the number of opcodes has no value by itself.

Calls and unwind: Fast paths must finish failure paths

Call optimization naturally draws attention to normal returns: one fewer allocation, direct access to a function body, or a reused frame. The difficult cases happen when execution leaves a call early.

The v1.6.1 experiments connect compiler-generated, independently verified call layouts with compact logical call records, exception-region tables, and a versioned unwind action table. Logical records preserve environment, argument, and target roots, together with restoration points for source, contract, effect, trace, and profiler state.

Normal returns, language Errors, cancellation, and host memory-quota jumps all need to restore registered state. Exited records must be cleared so stale references cannot keep completed environments alive.

This does not turn the interpreter into a trampoline. Machine-level calls and returns still use C and the existing setjmp/longjmp machinery. The addition is a logical record that the Runtime can verify, observe, and unwind.

Closures continue to capture a shared Env. Two closures reading and writing the same variable still refer to the same lexical environment. Environments captured by returned closures, closures passed through errors, or lazy Streams cannot be reclaimed into the frame pool. The optional bounded pool reuses only non-escaping environments, with limits of 64 Envs and 64 KiB of cached GC allocation capacity.

I did not simultaneously introduce flat upvalues or tail-call elimination. The former needs additional capture and aliasing proofs; the latter would change current recursion-depth and stack behavior. Each requires its own semantic and performance evidence.

Inline caches, GC, and scheduling need an admission case

The Map inline-cache experiment remembers a previously found slot without retaining a Map or Value. Every entry checks the current Map's count, the key length at that slot, and the complete key bytes before reading the current value.

It caches a checked location hint. Replacing the object, rearranging fields, or updating a value under the same key must never return an old result. Frequently changing sites fall back to generic lookup instead of accumulating unlimited cache shapes.

Observation needs equal care. guard_hit can describe a simulated candidate hit while caching is disabled. cached_reads records actual fast-path reads. Confusing them produces an impressive hit-rate report without demonstrating execution savings.

GC and scheduling decisions follow the same reasoning. HHY retains Boehm conservative GC and bounded process-based parallel with results delivered in input order. A slow first item creates head-of-line waiting; switching to completion order would change semantics.

This evaluation did not establish sufficient production bottleneck and requirement evidence for a generational collector, incremental collector, or async scheduler rewrite. Not admitting those changes today does not establish that they can never help. It means the architecture cost still needs a convincing case.

v1.7.0: Move provable transformations into compilation

The v1.6 work clarified the Runtime boundaries. In v1.7, I began moving repeated proof work into compilation.

The default remains Source → AST → direct Bytecode Compiler → Verifier → VM. The optional path introduces structured HIR and six optimization passes before Bytecode emission. Eligible functions can also carry independently verified integer register MIR plans. The AST engine remains a permanent semantic oracle and explicit fallback.

HHY v1.7: default Bytecode, optional HIR/MIR, and guarded execution

HIR is a versioned, non-SSA structured representation. It retains node relationships, source locations, local slots, types, constants, and effects. Its CFG describes statement control flow, loop phases, callable ownership, and conservative exception edges. Expressions preserve the language's evaluation order.

This is not a general SSA compiler, LLVM integration, or JIT. Integer MIR currently handles bounded straight-line expressions; other syntax continues through structured Bytecode. Plans are generated at compilation boundaries. The Runtime does not compile machine code on demand.

That scope lets me verify transformations within the existing language without introducing a second model of functions, errors, or memory.

The verifier must be able to reject the compiler

An optimizer labeling a value constant does not make it constant. Labeling an expression effect-free does not prove it can be deleted.

HIR tracks throw, cancel, allocation, and external effect separately. Unknown operations conservatively carry all effect bits. This means there is insufficient proof to delete or reorder them, not that every unknown operation necessarily performs every effect.

The verifier independently reconstructs types, constants, effects, CFG, region ownership, and local-slot provenance. Copy propagation must resolve to the nearest preceding immutable definition without crossing shadowing or accepting self-reference. Every enabled pass is followed by verification, followed by another check before emission and the existing independent Bytecode verifier afterward.

My requirement is that verification must not depend on trusting a pass to have run correctly. The boundary matters only if it can reject an incorrect compiler output.

Six passes, each with a defined proof boundary

The execution order is copy propagation, constant propagation, fold, peephole, unreachable, then DCE. Each pass can be disabled independently.

PassTransformationBoundary retained
Copy propagationFollow known immutable scalar copy chains in one regionNo shadowing, dynamic aliases, or unproven origins
Constant propagationPropagate known scalar bindings through a sequential regionNo mutable bindings or inference across call control flow
FoldEvaluate provably successful finite scalar operationsPreserve overflow, division errors, and allocation behavior
PeepholeSimplify proven Int +0, -0, and *1Unknown x + 0 is not automatically x
UnreachableRemove proven unexecuted branch bodies or terminating suffixesKeep conditions, loop headers, and scope capacity
DCERemove non-final, zero-effect pure value statementsKeep final values, declarations, and potentially effectful operations

An expression such as (10 + 20) * (4 + 6) offers a clear scalar proof. An unknown x + 0 cannot be simplified using mathematical intuition alone. Removing a block's final expression can change its implicit return value.

Executed allocations, closure captures, calls, and loop cancellation points remain. Fewer dispatches for successfully eliminated operations are expected, but error locations, exit codes, output, effects, quotas, and cancellation still require differential agreement.

MIR: Feedback proposes; guards authorize execution

Parameter type feedback enables integer MIR for supported functions. The current limits include eight parameters and 64 register instructions per target, with a bounded feedback target table.

A target must receive Int arguments on eight consecutive calls before specialization activates. Every subsequent entry still checks argument types. A type change immediately selects generic execution; eight accumulated type mismatches disable that slot.

Past stability does not prove future stability. Function rebinding must not reuse the old target's plan, so feedback identity is tied to the current chunk and function owner without retaining parameter or closure object references.

The fast path unboxes at entry, uses C-stack int64 registers for intermediate values, then boxes an Int or Bool result. Supported operations include checked integer addition, subtraction, multiplication, remainder, negation, and relational comparison.

When arithmetic fails, deoptimization preserves the failing instruction and operands, then resumes generic error handling at the original source operation. It must not restart the function and repeat allocations that already occurred.

Scalar replacement still preserves allocation reservations

The escape-analysis boundary is intentionally narrow: one local integer List, immediately accessed through a known valid constant index, with no other users. Nested aggregates, Maps, aliases, captures, returned aggregates, and values passed to calls conservatively fall back.

For an eligible shape, the selected element can return directly through registers, removing List element Value writes and reads. Every element is still evaluated in its original order, including elements that are not selected.

The implementation also preserves managed-storage reservations with the original size, scan kind, allocation order, and lifetime through the expression. Physically removing allocation could make a program succeed where it previously hit a memory quota, or change the lifetime observed by GC.

I therefore describe this as scalar replacement and reduced Value traffic, without claiming physical heap-allocation elimination or memory savings. That distinction matters to the credibility of a release note.

Performance admission: Synthetic gains cannot replace real gains

The compiler has a joint budget: compilation at most twice direct compilation plus 500 μs; no growth in candidate instruction count or logical Bytecode storage; Runtime at most 95% of baseline; managed allocation at most 101%. A candidate exceeding the code-size budget falls back entirely to direct Bytecode.

Runtime measurements include prepare time rather than only precompiled hot loops. Logical Bytecode bytes, reserved IR arena bytes, and peak process memory are separate metrics.

The following figures come from the final-release local macOS arm64 budget records, with two warmups per mode and 15 paired samples. Static IR and typed MIR were evaluated separately. They are not points on a version-to-version speedup curve. Ratios mean optimized mode divided by its comparison mode; lower is better.

Local static IR budget results: synthetic improvements do not establish general admission

WorkloadStatic IR ratioTyped specialization ratio
constant-loop, synthetic0.7491
propagation, synthetic0.9145
typed-arithmetic, synthetic0.8616
typed-scalar, synthetic0.8004
core-flow1.00430.9991
json-flow1.04291.0260
call-closure0.99291.0350

The static constant loop takes about 25.1% less time; the typed local-List example takes about 20.0% less. Yet the core, JSON, and closure workloads representing more general execution paths do not meet the 5% improvement requirement. Some regress.

The implementation has a measurable effect on specific shapes. That is not enough to make every user pay the additional compilation cost by default. These are also not whole-release comparisons between v1.5.0 and v1.7.0, and they do not establish overall business-application speedups.

Passing four platforms is separate from enabling by default

The final release records include CI and release acceptance for macOS arm64, Linux arm64, Linux x86_64, and Windows x86_64. Correctness, packaging, and compatibility passing do not establish admission for general performance benefits across platforms.

Validation covers AST, direct Bytecode, and IR Bytecode differential execution; all 64 combinations of six passes; independent IR/MIR mutations; sanitizers; fuzzing; quotas; cancellation; and real projects. Recorded evidence includes 10,000 IR field mutations per build and 285 additional typed-specialization execution paths.

During release work, I also corrected measurement and test methods. Profiler timing moved to interleaved paired sampling while retaining every sample and the existing thresholds. Repeated full executions of a large workload used a smaller equivalent case, while the original large cancellation case and real SIGINT validation remained.

These changes make a measurement answer the question it is meant to answer. The final default remains direct Bytecode, with AST available and the new optimizations explicitly enabled.

I want the next optimization to remain switchable

The controls expose the experiment boundaries directly. Here, program.hhy stands for the application script being evaluated:

# Enable the structured IR compiler
HHY_COMPILER=ir hhy run program.hhy

# Keep the IR pipeline but disable every static pass
HHY_COMPILER=ir HHY_COMPILER_DISABLE=all hhy run program.hhy

# Enable typed integer specialization and eligible local List scalar replacement
HHY_COMPILER=ir HHY_FEEDBACK_SPECIALIZATION=1 HHY_SCALAR_REPLACEMENT=1 hhy run program.hhy

# Inspect compilation metrics and reports
HHY_COMPILER=ir HHY_COMPILER_REPORT=1 hhy bytecode --metrics program.hhy

From v1.5 resource lifetimes through v1.6 profiling and Runtime experiments to v1.7 HIR/MIR, I have been building a method that can support further work: identify a cost, define supported shapes, verify independently, retain observations, then decide admission per platform and workload.

An optimization can be implemented, verified, and released while remaining disabled by default. That can be a complete and responsible engineering outcome. I want future HHY improvements to explain where they help, what they cost, and how execution returns to a reliable path when their assumptions no longer hold.

05 / 41

HHY Database 1.0: Building Database Access for Long-Running Applications

What Go database/sql, Python DB-API, and PHP PDO teach me about HHY's database layer: connection ownership, bounded pools, transaction scopes, streaming, exact types, and uncertain commits.
Sep 07, 2026 · 18 min read

After working on HHY Web Runtime (Chinese article), I started looking more closely at the database layer.

A handler can remain loaded and a route can return JSON, but a real CMS soon asks harder questions. Can saving an article also save its revision atomically? Will exporting hundreds of thousands of records keep consuming memory? When a request times out, has its database write actually stopped?

Those questions shape what I want HHY Database 1.0 to mean: an application should be able to understand the outcome of an operation and know who is responsible for closing its connection, transaction, and result set.

DB 1.0.0 shipped on September 7, 2026 alongside HHY 1.5.0. The database extension is independently versioned. Packages support macOS arm64 and Linux arm64/x86_64; the Windows Runtime archive does not include DB. Transaction callbacks, scoped resources, and database Streams require HHY 1.5.0 or later. Runtime release notes

This article reviews the shipped implementation and its tradeoffs, checked against repository snapshot 3af2f67. Performance figures come from the existing acceptance record, not a new benchmark run performed while writing this article. HHY Database 1.0 architecture: business logic in the host, connections in the extension

From four calls to managed resource lifetimes

Version 0.2.0 used a C extension with libpq and the MySQL client library, exposing ping, query, execute, and transaction. Value binding already existed: PostgreSQL used $1; MySQL used ? with native statement binding.

Version 1.0 preserves those four entry points and the default String/null result contract. It adds structured data sources, bounded pools, read/write transactions, savepoints, reusable statements, batches, incremental cursors, exact types, and cancellation cleanup. Extension changelog

I particularly like that compatibility does not require hiding every distinction. Existing scripts keep their result format. New code explicitly selects typed: true or Stream. Remote connections use authorized configuration and a TLS policy instead of quietly ignoring unfamiliar URL options.

The meaningful change is that connections, transactions, cursors, and statements now have lifetimes that can end, expire, and be cleaned up. Those are guarantees a persistent service needs.

Compare responsibilities across Go, Python, and PHP

A database driver is not the language itself. The Go comparison is database/sql plus a driver. Python means DB-API plus a driver and pool component. PHP means PDO plus its database driver. Calling all three a language-provided connection pool obscures meaningful differences.

ReferenceExisting abstractionWhat I take from it
Go database/sqlsql.DB manages a pool; sql.Tx binds a transaction connectionSeparate pool, connection, and transaction responsibilities
Python DB-API 2.0Connection, cursor, binding, transaction, and exception contractsStable behavior matters more than identical syntax
Python Psycopg PoolA separate pool component with scoped borrowing and returnBoth normal and exceptional exits need cleanup
PHP PDOA common access interface for preparation, transactions, and connection optionsReduce repetitive plumbing while retaining driver differences

These distinctions are defined in the respective interfaces and documentation. Go connections, Go transactions, PEP 249, Psycopg Pool

PDO persistent connections allow reuse, but their lifecycle depends on the process model. They are not interchangeable with Go's concurrent pool. Reuse also makes session cleanup important. PDO connection management

I did not reproduce three sets of APIs in HHY. The useful comparison is whether ordinary application work gets explicit, testable behavior for connections, transactions, types, and errors.

A pool manages borrowing and return

Short connections repeatedly pay for connection establishment, authentication, and potentially TLS negotiation alongside a small query. Reuse reduces that repeated work. Keeping connections in an array does not, by itself, produce a reliable pool.

The 1.0 pool follows an explicit lifecycle:

Wait for capacity → Borrow → Exclusive use → Clean session → Return
                                                └→ Uncertain state → Discard

Before return, the implementation must deal with unconsumed results, unfinished transactions, and modified session settings. A request should not silently inherit a previous request's time zone or transaction state.

Pool identity also cannot be just the host. Database, user, TLS policy, and credential version affect whether a connection is suitable for reuse. Sharing a pool across incompatible identities turns a performance optimization into an isolation problem.

I care more about identifying connections that must not return than about minimizing every connection close. Discarding an uncertain connection costs another handshake, but avoids passing a failure to the next borrower.

Version 1.0 can serve up to eight concurrent protocol requests per extension, with at most 64 queued calls. Each data-source pool defaults to four open connections and permits 1–32; the extension also has a 64-native-session aggregate limit.

A single HHY call remains synchronous. Web concurrency comes from workers, and database calls are not advertised as freely transferable HHY parallel-closure work. Protocol concurrency, pool capacity, and language-level concurrency are separate limits. Connection and concurrency contract

With multiple workers, connection budgets multiply

After introducing multiple Web Runtime workers, I cannot size a database pool by looking at one process.

Consider two application instances, four workers each, one database extension instance per worker, eight connections per data-source pool, and two data sources:

Total connection budget = 2 × 4 × 1 × 8 × 2 = 128

This is an illustrative aggregate budget, not a default configuration. The connections may target different servers; each server's capacity must be checked against the pools that actually connect to it. Overlapping old and new instances during deployment, migrations, and administrative scripts add to the budget.

Worker multiplication and exclusive transaction connection ownership

HHY 1.5.0 lazily starts each Web worker's extension after fork, without sharing parent pipes or database sockets. Saturation has bounded waiting, with DB_POOL_TIMEOUT and DB_QUEUE_FULL distinguishing failure modes. MySQL cancellation also uses a short-lived control connection, which needs additional server capacity. Concurrency and cancellation

A small pool also exposes an easy deadlock: a transaction owns the only connection, then code inside it issues a query through the ordinary pool. It waits for the connection it already holds. Go's documentation explicitly describes the lock-like waiting relationships introduced by connection limits. Go connection management

Transaction operations should therefore use their bound connection rather than borrow again.

Transactions must keep business decisions on one connection

Publishing an article can involve reading its version, checking for another editor's changes, updating the content, inserting a revision, and committing.

A fixed SQL list works when every operation is known in advance. Reading and branching require a scoped transaction.

The following simplified control flows show the same idea in three ecosystems. They omit connection setup, application SQL, and complete error handling; they illustrate ownership rather than provide deployable application code:

// Go: all transaction work goes through tx.
tx, err := db.BeginTx(ctx, nil)
if err != nil { return err }
defer tx.Rollback()

if err := updateArticle(ctx, tx); err != nil { return err }
return tx.Commit()
# Psycopg Pool: the borrowed connection scopes the transaction.
with pool.connection() as conn:
    update_article(conn)
# On normal exit, an open transaction commits; on exception, it rolls back.
// PDO: use the same connection and explicitly handle failure.
$pdo->beginTransaction();
try {
    updateArticle($pdo);
    $pdo->commit();
} catch (Throwable $error) {
    if ($pdo->inTransaction()) {
        $pdo->rollBack();
    }
    throw $error;
}

Go supplies a transaction object, Psycopg Pool supplies a connection context, and PDO exposes explicit transaction methods. Attempting rollback in a catch block does not establish that a COMMIT whose response was lost failed. Go transactions, Psycopg Pool, PDO transactions

HHY crosses an additional process boundary: the business function runs in the host, but the connection belongs to the extension. Version 1.5.0 keeps the closure in the host and uses with_transaction to bind its database operations to one transaction resource.

This is supported MySQL transaction syntax in 1.0. config is trusted application configuration, never supplied by an HTTP request; parameter values are illustrative:

import database

let before = config |> database.with_transaction { tx ->
    let current = database.query(tx,
        "SELECT balance FROM accounts WHERE id = ? FOR UPDATE", [1])
    database.execute(tx,
        "UPDATE accounts SET balance = balance - ? WHERE id = ?", [10, 1])
    current.rows
}

The trailing callback belongs to pipeline syntax, not database.with_transaction(config) { ... }. Normal callback completion commits; failure rolls back. Every operation uses tx. This illustrates scope; a real debit also needs balance, account-state, and idempotency checks. Transaction API

Resource tokens are randomly generated by the extension and bound to the host request scope. They cannot be transferred across requests or workers or serialized as durable IDs. Transactions and cursors retain leases tied to their initial operation deadline; closed resources become invalid.

Web requests and embedded hhy_call boundaries clean up leaked database resources on success and failure. Recovery belongs to request execution rather than timely garbage collection. When a connection is lost, attempting rollback still cannot prove that the database returned to its previous state. Host integration

Savepoints also need their actual database semantics. They allow an outer transaction to return to an intermediate point; they are not independently committed nested transactions.

Streaming must start at the database read

HHY's Flow and Stream abstractions fit an export pipeline well. But it is easy to build a superficially streaming interface: buffer the complete result in the driver, wrap it in a Stream, and let the application iterate.

That changes consumption syntax without changing peak memory.

The 0.2.0 analysis identified complete-result buffering. Version 1.0 retains a bounded ordinary query, but provides incremental cursors and a lazy Stream for larger results. Ordinary queries return at most 10,000 rows and may hit the byte budget earlier; callers must inspect truncated.

The lazy database Stream propagates demand upstream:

HTTP client can receive more
    → Output requests another batch
    → HHY Stream requests fetch
    → Extension reads a bounded database batch
    → Transform and write, then wait for further demand

Demand and cleanup propagate through the entire database export pipeline

Each batch needs row, byte, and field-size limits, as well as an overall execution budget. One large text row can outweigh a thousand small records. Protocol v1's 1 MiB line limit means that JSON and binary-encoding expansion also belong in the budget.

fetch defaults to 100 rows. Fields are limited to 64 KiB, with approximately 128 KiB decoded and 256 KiB encoded row or batch budgets. These limits apply together; hexadecimal binary encoding also consumes protocol capacity.

This is not a promise that arbitrary data can never exceed a transient memory target. The README explicitly notes that libpq can receive a large row before the field limit is checked. Streaming memory depends on the batch and the largest incoming driver row, not just the rows retained afterward. Result limits

Early take, exceptions, and request completion trigger cleanup. An unfinished or poisoned connection cannot become idle pool capacity. Early result termination inside an owning transaction can invalidate the transaction and require rollback, so callbacks must consume their Streams before returning.

Streaming also has a cost: slow consumers occupy connections and can extend transaction snapshots. Deadlines, export concurrency budgets, and sometimes separate background jobs remain application concerns. Stream lifecycle

Exact types matter more than convenient numeric conversion

Returning strings or null has one practical advantage: the implementation has not casually converted exact decimals to floating point. Applications still need to distinguish money, identifiers, dates, and ordinary text.

Version 1.0 enables typed results explicitly with typed: true. Representable scalars become HHY Int/Float/Bool. Decimal, oversized integers, temporal values, and JSON retain explicit type / value representations. Type contract

Data1.0 guarantee
DecimalExact decimal representation without a Float detour
Large integer IDsNo intermediate floating-point conversion; explicit lossless representation beyond HHY Int
nullDistinct from an empty string, zero, or no result row
TimeExplicit zone semantics and invalid or zero-date behavior
BytesBounded hexadecimal envelopes converted to native BytesBuffer by the host
Duplicate column namesPositional access or an explicit conflict, never silent overwriting

For example, converting 9007199254740993 through binary64 can lose information that converting back to a string cannot recover. A protocol's convenient universal number type can accidentally change a business identifier.

Generated identifiers belong to the operation that produced them. MySQL results must come from the corresponding write connection; PostgreSQL can express the result through RETURNING. Borrowing an arbitrary connection afterward to ask what was just generated is not a sound abstraction.

Affected rows deserve equal care. The current MySQL configuration uses CLIENT_FOUND_ROWS. Matching a row whose values did not change is different from modifying a row. A common access interface should not erase that distinction.

Before retrying, establish whether retry is justified

One error category I particularly value is DB_COMMIT_UNKNOWN.

Suppose the database receives COMMIT and completes it, but its acknowledgment is lost. The application sees a timeout. That timeout alone cannot establish that the transaction failed.

Automatically replaying the write could create another revision, decrement stock again, or duplicate a business record.

Version 1.0 distinguishes failure stages; applications must use that information when choosing a response:

Failure pointWhat the application can decide
Waiting for pool capacity; SQL not sentConsider another attempt within the remaining deadline and business policy
Authentication or TLS verificationCorrect configuration instead of repeating a doomed connection attempt
Constraint conflict, deadlock, serialization failureHandle the particular transaction state and business semantics
Connection loss during executionTreat the outcome as potentially unknown; do not replay writes by default
Connection loss during commitReport commit uncertainty and reconcile using idempotency identifiers or business records

The extension does not replay application SQL after errors. Connection loss or timeout during commit reports DB_COMMIT_UNKNOWN, not a successful rollback. Transaction and error semantics

Cancellation has the same distinction. A timeout received by the caller means it stopped waiting, not necessarily that the database stopped executing. Go Context provides a way to propagate cancellation; HHY 1.5.0 connects Runtime cancellation with its database extension. Go database cancellation

MySQL cancellation uses a separate short-lived control connection. libpq 17+ uses asynchronous cancellation; older versions use the legacy API. Failed cancellation can guarantee connection disposal, not immediate completion of server-side work. Applications must reconcile uncertain writes.

Timeouts also participate in resource leases. Native HHY Duration values convert to milliseconds, making timeout_ms: 5s valid configuration. The option name contains _ms, but application code can still use the language's time units. MySQL native connection and socket settings retain second-level granularity, so a common option does not imply identical driver precision. Time and cancellation contracts

Cloud connections and a CMS make the abstractions concrete

Remote sources now use structured configuration with explicit host:port authorization through allow. Legacy URLs remain loopback-only. Authorization comes from trusted script configuration, not an OS network sandbox. HTTP requests must not directly supply credentials, SQL, or an allow-list. Data-source configuration

TLS distinguishes required, verify_ca, and verify_identity, among other modes. Identity verification is the default, and verification failure does not fall back to plaintext. Builds using the MariaDB client support identity verification but reject CA-only verification because the client interface differs.

RDS can be configured as a remote source, but actual RDS networking, authentication, and failover acceptance still awaits a dedicated instance. Shipping remote connectivity is different from certifying a particular cloud deployment. Connection boundaries

The CMS install.hhy workflow tests another boundary. The driver executes operations and reports reliable outcomes. The installer owns migration order, progress records, recovery, and configuration writes.

Some MySQL DDL implicitly commits, so wrapping every table-creation statement in a transaction cannot guarantee atomic installation rollback. The installer should record completed steps and verify them before resuming. MySQL implicit commits

This first application also helps constrain scope. DB 1.0 should make ordinary MySQL and PostgreSQL access dependable. ORMs, sharding, and distributed transactions do not belong in the driver merely because other ecosystems offer them.

The evidence I find most useful

The acceptance record covers pool, transaction, cursor, and type behavior against MySQL 8.4 and PostgreSQL 17, plus AST/Bytecode execution, two prefork workers, abandoned-resource cleanup, TLS negative cases, and extension crash recovery. CMS tests use a minimal installation fixture covering interruption, repeated installation, failed upgrades, and logical backup/restore; a complete CMS is not bundled with the extension. Acceptance record

The local macOS incremental-result test used a 64-byte payload plus an ID per row:

Database100,000 rows1,000,000 rowsExtension peak RSS: smaller / larger result
MySQL0.210 s1.961 s14,024,704 / 14,254,080 bytes
PostgreSQL0.172 s1.585 s23,707,648 / 23,937,024 bytes

For both paths, ten times as many rows increased measured extension peak RSS by only 229,376 bytes, or 224 KiB, on this workload. That is more informative about incremental consumption than a standalone rows-per-second figure. It measures the extension, not the complete Web service, and does not guarantee the same behavior for arbitrary row widths or query plans. Measurements and reproduction

A separate 300-second local single-client test completed 1,583,501 SELECT iterations on MySQL and 2,752,244 on PostgreSQL. Each ended with one open connection and zero pinned or in-use connections. This supports connection reuse and return behavior; it is neither multi-worker CMS throughput nor a Go/Python/PHP comparison.

A 24-hour soak and real RDS failover validation remain outstanding. Cross-language performance work still needs equivalent databases, SQL, indexes, TLS, network conditions, and aggregate connection budgets, with process models disclosed. Acceptance scope

The opportunity in HHY Database 1.0 is to connect Flow, Stream, structured errors, and isolated extensions to database semantics that survive long-running application work. A successful query is easy to demonstrate. Understanding what happened after a pool fills, an export stops, or a commit response disappears is what would make me comfortable attaching real business data.

References and baseline

DB 1.0.0 shipped on September 7, 2026 alongside HHY 1.5.0. This article starts from the earlier design document and checks the delivered scope against the formal documentation and acceptance record at repository snapshot 3af2f67f8a889afd65a0215dd5423430e7ea677e.

06 / 41

GPT, Down to the Scalar: How I Read MicroGPT

Tracing Karpathy's MicroGPT from characters and computation graphs through causal attention, cross-entropy, Adam, and sampling—with worked examples and diagrams of the complete learning loop.
Sep 07, 2026 · 22 min read

When I wrote about training a Transformer from scratch, I focused on the engineering chain: preparing data, choosing a tokenizer, saving a model, and understanding why low loss did not guarantee useful answers. Reading Karpathy's MicroGPT made me want to go one level deeper.

If I remove MLX, PyTorch, tensor operations, and the GPU, what remains of GPT training?

My answer is a function that assigns probabilities to the next token, a graph that records how its calculations depend on one another, and a rule for changing parameters in response to error. At this scale, I can trace an individual multiplication all the way to an output probability.

This article uses source snapshot 14fb038 as its reference. The derivations, worked examples, and diagrams below are my explanation. The numerical examples illustrate calculations; they are not training results. A name generator also should not be mistaken for a general language assistant.

The MicroGPT loop: characters to probabilities, loss back to parameters

Start with the prediction task

Take one training example, emma. The model learns a sequence of conditional probabilities:

P(emma, END | START)
= P(e | START)
× P(m | START,e)
× P(m | START,e,m)
× P(a | START,e,m,m)
× P(END | START,e,m,m,a)

This is the probability chain rule applied to a sequence. GPT implements each conditional distribution with the same parameters. Different prefixes produce different probabilities.

In this implementation, characters map to integer IDs, and the special token BOS appears at both ends. START and END in the explanation therefore share one ID. The model learns both how to begin and when to stop.

PositionCurrent inputVisible prefixTarget
0BOSBOSe
1eBOS em
2mBOS e mm
3mBOS e m ma
4aBOS e m m aBOS

The target shifts one position ahead, and attention can only use current and earlier inputs. Both conditions are necessary. Without the shift, a model could learn to copy. With access to future inputs, it could inspect the answer.

During training, the next input is the real character from the data: teacher forcing. Even if the model guesses the wrong character, the following position still receives the correct one. During generation, the next input is the model's own sample. A mistake then changes the prefix used for every subsequent prediction.

How does a parameter know which way to move?

For me, the best entry into MicroGPT is a multiplication node, before attention enters the picture.

Let a = 2, b = 3, u = a × b, and L = u + a. The forward result is 8. Backward computation asks how much L would change if a or b changed slightly:

∂L/∂u = 1
∂u/∂a = b = 3
∂u/∂b = a = 2

∂L/∂a = (∂L/∂u)(∂u/∂a) + 1 = 4
∂L/∂b = (∂L/∂u)(∂u/∂b)     = 2

There are two paths from a to L. Their contributions must add. Replacing gradient accumulation with assignment silently loses a path.

Two paths contribute to the gradient of a shared scalar

An automatic differentiation scalar needs four kinds of information: its current value, its accumulated gradient, its input nodes, and its local derivatives with respect to those inputs. Forward operations record dependencies. Backward operations propagate gradients through them.

Local operationLocal derivative
z = a + b1 for each input
z = a × bb with respect to a; a with respect to b
z = a^kk × a^(k−1), for constant k
z = log(a)1/a
z = exp(a)exp(a)
z = ReLU(a)1 above zero, 0 below; choose 0 at zero

These rules compose into linear layers, normalization, softmax, and loss. A matrix is a container of scalars; its multiplication can be expressed as individual products and sums.

Backward propagation runs in reverse topological order. A node must collect all contributions from operations that consume it before passing the accumulated result to its inputs. Set ∂L/∂L = 1, then apply:

input gradient += output gradient × local derivative along this edge

Deduplicate nodes when ordering the graph, but preserve repeated input edges. In a × a, both edges contribute, producing 2a.

Parameter sharing follows the same rule. One embedding row can participate at multiple positions. It remains one set of parameter objects, and gradients from those positions accumulate into them.

Account for every parameter

The reference configuration has one layer, hidden width 16, four heads, and 16 context positions. Each head has width 4; the MLP expands to width 64. Let V be the vocabulary size. Matrices are stored as output width by input width.

ParametersShapeCount
Token embeddingV × 1616V
Position embedding16 × 16256
Q, K, V, O projections16 × 16 each1,024
First MLP projection64 × 161,024
Second MLP projection16 × 641,024
Output LM headV × 1616V
TotalNo biases or learned normalization gains32V + 3,328

For 26 letters plus BOS, V = 27 and the total is 4,192 parameters. This is a calculation from the shapes, not a constant for every custom dataset. The token embedding and output head are separate matrices in this version; their weights are not tied.

An integer token ID is an address, not a semantic coordinate. Character 20 is not inherently twice character 10. The ID selects a learned vector. Adding a learned position vector lets the representation distinguish where the character occurs.

Follow one forward pass

I use column vectors for a single position to keep the data flow visible. Let R denote RMSNorm, E the token embedding table, and P the position table:

x₀ = R(E[token] + P[position])

u = R(x₀)
q = Wq u,  k = Wk u,  v = Wv u
h = x₀ + Wo · MultiHeadAttention(q, K≤t, V≤t)

r = R(h)
x₁ = h + W₂ · ReLU(W₁ r)

logits = Wout x₁
probabilities = softmax(logits)

This describes the one-layer implementation. Do not silently insert a final normalization: this snapshot sends the last residual output directly to the LM head. It is a simplified decoder-only model, not an exact GPT-2 reproduction.

One-position forward computation and the two residual paths

RMSNorm controls the scale

For a vector with d elements:

mean_square = (x₁² + ... + x_d²) / d
R(xᵢ) = xᵢ / sqrt(mean_square + ε)

For [3, 4], ignoring the small epsilon, the denominator is sqrt(12.5) and the result is approximately [0.8485, 1.1314]. This does not subtract the mean or make all elements identical. It adjusts the overall scale while preserving direction.

MicroGPT's variant omits a learned gain vector and uses epsilon 1e-5.

Two normalizations appear near the beginning, which can look redundant. But the first normalized vector also enters the residual path; the second normalization belongs only to the attention branch. Similar forward values do not justify removing the first operation: that changes the residual stream and its gradient path.

Residuals let a block learn a correction

For y = x + F(x), the derivative includes a direct path: ∂y/∂x = I + ∂F/∂x. Existing information can survive while a block adds a correction. This helps gradient propagation, but does not guarantee stable training at arbitrary depth or learning rate.

What attention actually computes

I think of Q, K, and V as three roles. Q determines how the current position searches for information. K determines how a visible position participates in matching. V carries the content to be combined. These are learned projections, not fields with meanings assigned by the programmer.

For position t in one head:

score(t,j) = dot(q_t, k_j) / sqrt(d_head)     j ≤ t
weight(t,j) = exp(score(t,j)) / Σ exp(score(t,r))
output_t = Σ weight(t,j) × v_j

The normalization and weighted sum run only over visible positions. Four heads each return four numbers. Concatenating them gives 16 numbers, followed by an output projection. This applies scaled dot-product and multi-head attention at a very small width.

Why divide by the square root of head width? Under idealized assumptions of independent, zero-mean components with suitable variance, dot-product variance grows with dimension. Scaling helps prevent dimension alone from pushing softmax toward overly sharp distributions. With head width 4, the divisor is 2, not 4 or 16.

Here is a constructed example. Use q = [1, 0, 1, 0] and three visible keys: [1, 0, 0, 0], [0, 1, 0, 0], and [1, 0, 1, 0]:

Dot products     = [1, 0, 2]
Scaled scores    = [0.5, 0, 1]
Softmax weights  ≈ [0.3072, 0.1863, 0.5065]

For values:
[1, 0, 0, 0], [0, 2, 0, 0], [0, 0, 3, 0]
The output is approximately:
[0.3072, 0.3726, 1.5194, 0]

Attention blends information instead of selecting just one position. Its weights are also not output-token probabilities. Attention normalizes over visible positions; the language-model output normalizes over vocabulary entries. Projections, residuals, and an MLP sit between them.

Causality without an explicit mask

A parallel implementation usually constructs scores for all position pairs, then assigns negative infinity to future positions so their softmax weights become zero. MicroGPT advances one position at a time. It computes the current K and V, appends them to the current layer's lists, and attends only to entries already present.

At position t, those lists contain positions 0...t. Future information is absent from the accessible data structure. Execution order enforces causality.

Causal visibility and KV entries that retain their training graph

During training, these K/V entries remain attached to the computation graph. A later position's loss can propagate through attention into earlier K/V projections and embeddings. Preventing forward access to future inputs does not prevent a later loss from sending gradients to earlier computations.

Each document or generated sample needs fresh lists, and each layer needs its own lists. At inference, historical K/V can be cached as values without gradients. Detaching them during training would cut required gradient paths.

This is not an automatically sliding, unlimited context window. There are only 16 learned position rows. Training uses at most the first 16 prediction positions, and generation runs at most 16 iterations. Longer custom documents do not automatically become multiple windows. They need an explicit slicing policy; otherwise even the closing boundary token may be excluded from training.

Connect the MLP, softmax, and loss

Attention mixes information across positions. The MLP applies a nonlinear transformation to each position's representation: 16 dimensions to 64, ReLU, then back to 16.

Without a nonlinearity, two linear transformations collapse into one: W₂(W₁x) = (W₂W₁)x. ReLU prevents that general collapse and allows different feature combinations to activate for different inputs.

The LM head produces one logit per vocabulary entry. Logits can be negative and need not sum to one. Softmax turns them into probabilities:

pᵢ = exp(zᵢ − c) / Σⱼ exp(zⱼ − c)
c = max(z)

Subtracting a common constant leaves the probabilities unchanged because the common factor cancels. Choosing the maximum prevents large positive exponential arguments. Treating c as an ordinary number preserves the correct derivative here because softmax is invariant to a shared shift. Extremely small probabilities can still underflow; production training typically uses a stable log-softmax or fused cross-entropy rather than computing tiny probabilities and then taking their logarithm.

For target y, the position loss is L_t = −log(p_y). Raising the correct token's probability from 0.1 to 0.5 lowers loss from about 2.3026 to 0.6931. The document loss averages all valid prediction positions, not just the final character.

The derivative is particularly useful. Starting from L = −z_y + log(Σ exp(z_j)), differentiation gives:

∂L/∂zᵢ = pᵢ − 1[i = y]

For predicted probabilities [0.2, 0.5, 0.3] with the first entry correct, the logit gradient is [-0.8, 0.5, 0.3]. Gradient descent directly raises the correct logit and lowers the others. The chain rule carries that signal back to the contributing parameters. Averaging over n positions divides each position's contribution by n.

Uniform guessing over 27 classes gives ln(27) ≈ 3.2958 loss and perplexity 27. That is a theoretical reference, not an exact prediction of the first printed loss: random initialization need not produce a perfectly uniform distribution.

How Adam turns gradients into updates

Plain gradient descent uses θ ← θ − ηg. Adam additionally maintains exponential moving averages of gradients and squared gradients, adapting update scales per parameter. The update can be expanded directly.

Let s count optimizer steps starting at 1, separately from token positions:

m_s = β₁ m_(s−1) + (1−β₁) g_s
v_s = β₂ v_(s−1) + (1−β₂) g_s²

m̂_s = m_s / (1−β₁^s)
v̂_s = v_s / (1−β₂^s)

θ_s = θ_(s−1) − η_s × m̂_s / (sqrt(v̂_s) + ε)

Bias correction compensates for moments initialized to zero. The source uses beta values 0.85 and 0.99, initial learning rate 0.01, and linear learning-rate decay. This is Adam, without AdamW's decoupled weight decay.

For a first gradient of 0.2, m becomes 0.03 and v becomes 0.0004. Their corrected values are 0.2 and 0.04. Ignoring the tiny epsilon, the first update subtracts approximately 0.01. A larger raw gradient therefore does not automatically imply a proportionally larger Adam step.

The training loop can be expressed independently as the following pseudocode. It is a reading aid, not a runnable GPT implementation:

Initialize parameters θ and Adam state m, v
For each step:
    Select one document, encode it, and add boundary markers
    Create fresh K/V lists for every layer
    Compute next-token losses using the real prefix at each valid position
    Average the losses and backpropagate once
    Update θ with Adam and clear parameter gradients

Averaging each document before one update makes the document the sampling unit. This is not automatically equivalent to averaging every token in the entire corpus: document length changes the relative weight of an individual token.

The default 1,000 steps are not 1,000 epochs. A step processes one document; an epoch requires a full pass through the dataset. The decay formula also uses a zero-based step index, so the last update has a small positive learning rate rather than exactly zero.

During generation, learning has stopped

Start with BOS, compute logits, apply temperature, sample a token, and feed it into the next position. Stop on BOS or after at most 16 generated characters.

pᵢ(T) = exp(zᵢ / T) / Σⱼ exp(zⱼ / T)    T > 0

For two logits separated by 1, the probability ratio is approximately 2.72 at T = 1 and 7.39 at T = 0.5. Lower temperature concentrates the distribution. It does not add knowledge or guarantee factual correctness. Temperatures above 1 are valid; zero cannot be inserted into this division and normally requires a separate greedy-selection path.

Inference performs no loss backward pass or Adam update. Reading the prefix changes activations and cache contents, not long-term parameters. This minimal implementation reuses autograd operations, so it still incurs graph-building overhead. Omitting backward is not equivalent to implementing a framework's no-grad mode.

A plausible name suggests that some character regularities were learned. Novel-looking output alone does not establish that the model avoided memorization. Generalization needs held-out examples, overlap checks, and evaluation aligned with the intended task.

How I would verify an implementation

Short code can still be wrong. I would start with calculations I can check by hand, then look at training curves.

  1. Shared-node gradients: L = a×b+a at a=2, b=3 must yield gradients 4 and 2. a×a must yield 2a.
  2. Numerical gradients: compare selected automatic derivatives with central differences, (L(θ+h)−L(θ−h))/(2h), avoiding ReLU's kink at zero.
  3. Causality: changing a suffix while keeping the prefix fixed must leave prefix logits unchanged. A parallel rewrite should also agree with the sequential version.
  4. Loss and updates: check probability normalization, cross-entropy derivatives, the first Adam update, and gradient clearing after every step.
  5. Data boundaries: verify BOS pairing, truncation, and vocabulary coverage before trying tiny-set overfitting and independent validation.

I prepared a dependency-free calculation check for this article. It covers shared-node backpropagation, central differences, the attention example, cross-entropy gradients, and the first Adam update. It checks the mathematical examples, not complete GPT training or performance.

To run the author's implementation, save the pinned source as microgpt.py in a separate empty directory and execute python3 microgpt.py. If input.txt is absent, the script downloads the default names dataset. A custom file should contain one nonempty example per line; keep the character vocabulary and 16-position truncation in mind. The current Gist and its revisions can be compared with the pinned version.

What a complete algorithm means to me

MicroGPT connects ideas that are often learned separately into one traceable path. Characters select input vectors. Attention mixes context. The output layer assigns probabilities. Loss produces gradients. The optimizer changes parameters. Those changes affect the next prediction.

Reaching a useful language model still requires decisions about data quality, tokenization, architecture, training objectives, evaluation, and post-training. These cannot all be reduced to execution speed. GPUs, batching, and fused kernels primarily change how efficiently computation runs. Different data and supervision change what the model learns.

For my own engineering work, the code suggests a practical debugging order. Wrong predictions: inspect target alignment. No learning: inspect gradient paths. Suspiciously good loss: check for future leakage. Strange samples: inspect probability conversion and stopping conditions.

This is the level of understanding I want to keep: relationships I can inspect. Knowing where a number came from gives me a reasoned way to decide what to change next.

References

07 / 41

From Scripts to a Resident Service: How I Built the HHY Web Runtime

A retrospective on the HHY v1.4.3 Web Runtime: from a repeatedly callable interpreter and HTTP request parsing to GET/POST, streaming backpressure, multiple workers, and the performance boundaries behind one million requests.
Sep 04, 2026 · 16 min read

In the previous two articles, I wrote about optimizing the AST interpreter and the Bytecode VM. At that point, the question I cared about was whether the same HHY program could run faster while preserving identical semantics.

With the Web Runtime, the question changed.

When a script finishes and the process exits, the operating system cleans up many lifecycle problems for you. A service, however, must stay alive: the ten-thousandth request must not read data left behind by the previous request, one handler exception must not bring down the entire process, and a slow-reading client must not cause the service to accumulate output without limit.

That is why my starting point for HHY Web was making the Runtime withstand repeated invocation. Only after that was true did routing, JSON APIs, and a framework have somewhere solid to stand.

The HHY v1.4.3 release verification includes two sets of numbers that matter to me: 100,000 repeated invocations in the same loaded context; and 1,000,000 local HTTP requests completed by 16 concurrent clients with zero failures in 131.439 seconds, or about 7,608.1 requests per second. The first validates resident invocation; the second validates a specific short-request workload. Neither is a performance promise for every web scenario. Release notes

HHY Web architecture: reverse proxy, C HTTP Server, resident Runtime, and thin framework

First, Turn the Interpreter into a Runtime That Can Be Called Long-Term

I divided the v1.4 work into four capability stages and delivered them together in the final v1.4.3 release. These versions mark development boundaries; they do not mean users need to install four versions in sequence.

StageFirst problem to solveCapability delivered
v1.4.0Can a program be loaded once and invoked repeatedly?opaque C handles, resident contexts, JSON ABI
v1.4.1How do network requests enter HHY?HTTP/1.1, Router, request and response objects
v1.4.2How does an API grow into an application?Middleware, static files, uploads, CORS, Cookie, development reload
v1.4.3How does a service withstand continuous operation?Stream, SSE, Range, multi-process Workers, logs and metrics

This order matters to me. If the initial focus is only GET /hello, it is easy to turn every request into a fresh script execution. The endpoint can return JSON, but process startup, source loading, and execution-environment initialization all end up in the request path.

The resident model moves that fixed work into the startup phase, after which requests repeatedly invoke a handler. The public embedding API uses HhyApplication and HhyContext to hide internal representations. The host passes arguments and retrieves results across a JSON boundary instead of depending directly on HHY's Value memory layout.

An application must outlive the contexts it creates, and a context cannot be called concurrently by multiple threads without coordination. These lifecycle constraints matter more than whether the API names look concise.

The web service likewise reuses prepared application state. AST and Bytecode still share the language semantics; Bytecode is the default engine, while AST continues to provide differential verification. Top-level mutable bindings are rejected in an embedding context so global variables cannot become implicit cross-request state. A handler exception returns 500, and later requests continue to be served. Runtime design

I Split the Runtime and Framework into Two Layers

HHY Language's built-in import web handles requests, routing, responses, Streams, and Workers. HHY Web 0.1.0, in a separate repository, is a layer of ordinary HHY code that requires language version v1.4.3 or later.

The framework adds application organization: a default Request ID, optional CORS and gzip, JSON errors, Bearer authentication, and more direct routing functions. The network stack and Runtime do not need to be implemented again.

For example, a Blueprint here is simply a function that accepts an application and returns an application:

fn api(application) {
    return application
        |> hhyweb.get("/api/books/:id", book)
        |> hhyweb.post("/api/books", create_book)
}

Calling that function from mount is enough. Module composition continues to use HHY's existing functions and Flow, without introducing another registration syntax. The framework's real value is reducing the repeated code every project would otherwise need while keeping execution readable directly from the source. HHY Web source

How an HTTP Request Reaches a Handler

Start by breaking down an ordinary request:

GET /api/books/42?lang=zh HTTP/1.1
Host: example.com
Accept: application/json

That first line contains three different dimensions: GET is the method, /api/books/42 is the path, and lang=zh is the query string. They ultimately affect route selection and handler input in different ways.

What the lower layer receives is not a "request object," but segments of TCP bytes. A single recv may not contain the complete request headers, or it may include part of the body as well. The parser must identify message boundaries itself.

The parsing path from TCP bytes to an HHY handler

The current C Server first accumulates data, looks for \r\n\r\n, and then separates the request line from the Headers. It checks the method token, a request target beginning with /, and the HTTP/1.1 version, then separates the path from the raw Query.

The request-body length is determined by Content-Length. The value must parse as a valid number and remain below the configured max_body; if the body is incomplete, reading continues. Exceeding the body limit returns 413, a read timeout returns 408, and malformed input returns 400. The network layer handles these boundaries before execution enters HHY business code.

One detail is easy to misunderstand: the current service supports chunked responses, but does not accept request bodies with Transfer-Encoding. Request and response capabilities are not symmetrical. An upload endpoint cannot assume it accepts chunked uploads just because the service supports streaming output.

Inside the Runtime, a request becomes a WebRequest: route parameters go into params, decoded query parameters into query_params, alongside headers, cookies, the text body, and binary bytes. After matching /api/books/:id, for example, 42 becomes request.params.id.

The Router must consider both method and path. No matching path is 404; an existing path with the wrong method is 405. Middleware can return a response early, such as rejecting a request with missing credentials, and calls the handler only after the request passes. Finally, the Runtime assembles the response, the C Server writes the Header and Body, and the connection ends. HTTP Server implementation

The Difference Between GET and POST Is More Than Where Parameters Go

When developing an API, I prefer to choose the method by asking what operation this request expresses.

MethodTypical useSafe / idempotent semanticsEntry point in HHY Web
GETRead a resource or query a listSafe and idempotenthhyweb.get
POSTCreate a resource or submit a one-time operationNot guaranteed safe or idempotenthhyweb.post
PUTCreate or replace resource state at the target URIUnsafe and idempotenthhyweb.put
PATCHPartially modify a resourceUnsafe and not guaranteed idempotenthhyweb.patch
DELETEDelete the target resourceUnsafe and idempotenthhyweb.delete

"Safe" here means the method semantics do not request a resource modification; it does not mean the endpoint is authenticated or its data encrypted. "Idempotent" means repeating the same request is expected to have the same effect; it does not require an identical status code and response every time. A DELETE that succeeds the first time and reports a missing resource the second time can still satisfy idempotent semantics.

Business code must uphold these semantics. Registering a charge operation under GET does not make it safe because get was used. If POST must support reliable retries, the application needs an idempotency key or deduplication mechanism. HTTP method semantics, PATCH semantics

Input locations and business semantics of GET, POST, and other methods

Query is not exclusive to GET; a POST URL can also contain a Query. JSON, forms, and binary data are request-body formats and cannot be inferred from the method name alone. A GET body has no generally defined semantics and practical APIs should avoid depending on it.

HHY Web's JSON helper is direct: it runs parse_json on request.body and turns a parse failure into a stable 400 JSON error. It does not perform business-field validation automatically, nor does this helper enforce Content-Type. An endpoint requiring strict media-type constraints should add that check at the application layer.

The create-book example in the repository demonstrates this boundary:

fn create_book(request) {
    let parsed = hhyweb.request_json(request)
    if not parsed.ok {
        return parsed.error
    }
    return hhyweb.created({ id: "new-book", book: parsed.value })
}

It demonstrates JSON parsing and a 201 response; it does not implement database persistence. In a real business integration, the next steps are field validation, storage, and returning the created resource.

HEAD and OPTIONS also deserve separate consideration. The former asks for response metadata without transferring the response body; the latter asks about communication options and is used by browser CORS preflight requests. The HHY Runtime provides CORS preflight support, but the current thin framework exposes the five routing helpers in the table above. The HTTP standard does not imply that every method has a framework function with the same name.

The Value of Streams Is Controlling Production Speed

An ordinary JSON response is generally serialized first and then sent as a Body of known length. When exporting a large amount of data or continuously pushing events, this increases both waiting time and memory use.

web.stream turns output into a chunked response. Its key constraint is that it requests the next item from the Stream only after the previous chunk has been written. Client read speed therefore influences server production speed through socket write pressure.

That is backpressure in this design. It prevents the network output side from prefetching data without limit, but if the application first creates a huge array and only then converts it to a Stream, the earlier memory cost still exists.

SSE adds an event format on top of a Stream and is suited to one-way notifications and incremental output. Static files also support streaming large files and a single byte Range; cache validation uses ETag / Last-Modified to reduce unnecessary entity transfer.

These mechanisms address different costs: Stream controls output accumulation, Range narrows the read range, and cache validation reduces repeated transfer. They cannot be collapsed into the claim that "streaming is supported, therefore performance is higher."

What One Million Requests Actually Demonstrate

The load-test results in the official release record are below. This cites existing release evidence; the test was not rerun for this article.

ItemRelease verification record
Total requests1,000,000
Concurrent clients16
Failures0
Total duration131.439 s
Throughput7,608.1 requests/s
Network scopeloopback, local machine

The load-test script provides the context needed to interpret these numbers. A Python client uses a thread pool; each request creates a new HTTP connection, sends GET /, reads the response, checks the status code and JSON content, and then closes the connection. The service comes from an acceptance fixture configured with two Workers, with access logs disabled during the test. The measurement therefore covers a short-request path including connection setup, protocol handling, the handler, JSON response generation, and client-side validation. Load-test script, service fixture

This is closer to a service than benchmarking an empty function, but it still does not cover database waits, public-network latency, TLS, complex business logic, large request bodies, or slow SSE clients. The Python load generator may also be one of the constraints.

The release record does not include the machine model, CPU utilization, RSS curve, or p95 / p99 latency, so I will not use it to rank HHY against Go, Node.js, or other frameworks. Nor can 1 / RPS be treated as average latency for a single request: this is a concurrent workload.

The approximately 2.7x gain in the previous Bytecode article came from a specific CPU Flow workload and likewise cannot be applied directly to HTTP. Total web-request time also includes socket reads and writes, parsing, allocation, routing, and response encoding. Resident loading removes fixed overhead; Bytecode shortens part of the execution path; the actual bottleneck still has to be measured layer by layer.

The Benefits and Limits of Multiple Workers Both Need to Be Explicit

The current source uses a prefork model: multiple Worker processes share the listening socket, while the parent monitors and replaces Workers that exit. After accepting a connection, each Worker synchronously completes the read, handler execution, and response write before accepting another connection.

There is a select call in the source, but it waits on the listening socket. It does not mean a non-blocking event loop has been implemented across all client connections.

The current response also explicitly uses Connection: close. In other words, this version's performance shape is multi-process, synchronous per-Worker handling, and short-lived connections. Adding Workers can increase concurrent handling capacity, but it does not automatically turn the service into a massive-connection scheduler; a connection that reads slowly or remains active for a long time still occupies one Worker.

This also explains why "supports SSE" and "can sustain a large number of online SSE connections" are two different stages. Backpressure controls output accumulation, but it does not eliminate connection occupancy. Supporting long-lived connections at scale requires further design for non-blocking I/O, connection scheduling, timeouts, and coordinated cancellation, followed by verification under a mixed slow-client workload.

The current implementation exposes another cost: for each connection, the receive buffer allocates capacity according to the configured Header and Body limits. max_body is therefore both an input boundary and an influence on allocation size; it cannot be increased without limit for convenience. Whether this should move to progressive allocation later must be decided from allocation hot spots and memory measurements.

I would first add measurements for tail latency, Worker occupancy, and RSS changes under a mixture of short requests and long-lived connections, then decide whether to optimize route lookup, allocation strategy, or the connection model. Optimization has direction only after the time-consuming layer is identified.

Putting the Service in Front of a Real Browser

The live Dashboard is a direct entry point into this path: an HHY handler returns HTML, and the browser then calls /api/status and /api/hello to display JSON.

It is useful for confirming that the page, routing, Query, and Request ID path is connected. However, the Runtime version, engine name, and Worker count in the example source are explicitly written display fields; those cards must not be treated as live process probes. Real operating status still requires health endpoints, logs, metrics, and process monitoring. Dashboard source

The production boundary remains explicit: an HHY service listens on an internal address, while Nginx, Caddy, or a cloud load balancer terminates TLS / HTTP/2; v1.4 does not include WebSocket. Enable trust_proxy only when every directly connected client comes from a trusted proxy.

During development, I care more about behavior after failure. Does invalid JSON return 400? Does an oversized Body return 413? Can the next request succeed after a handler throws? Are temporary upload files cleaned up on both success and failure paths? These facts say more about whether a resident service truly works than a home page displaying "server running."

From Executing a Program to Continuously Receiving Requests

After completing the Web Runtime, my expectations for HHY became concrete at another level.

Originally, Flow connected files, processes, HTTP clients, and Streams into a task. Now the same language capabilities can live inside a resident handler and provide a continuously running service. The framework stays thin, while the Runtime handles lifecycle, error isolation, and resource boundaries.

One million short requests with zero failures is one piece of evidence along this path. Synchronous Workers, short-lived connections, and long-stream occupancy also define the next set of questions clearly.

A Runtime can serve reliably over the long term not only because successful requests are fast enough, but because failure, waiting, and resource release all have a defined destination.

That is the capability I truly wanted to complete when moving from scripts to the web.

References

08 / 41

Travel Is Not a Checklist: How the Road Keeps Revising My Map of the World

Distance and cities are only traces. What travel truly leaves behind is the repeated collapse of assumptions—and a new understanding of places, people, technology, and myself.
Sep 02, 2026 · 7 min read

While reviewing my travel history, I found two sets of numbers: 60,580 kilometers across China, 54 journeys, and 13 cities; beyond China, 30,601 kilometers, nine journeys, five countries and regions, and eight cities.

Numbers create an easy sense of completion. The lines on the map grow denser. The list of places gets longer. It begins to look as if I am gradually collecting the world into a résumé.

I now prefer a different interpretation: these lines record more than where I have been. They trace the ways reality kept revising what I thought I knew.

Travel is not about checking places off a list. It is about continuously revising what I imagined the world to be.

My travel routes across China over the years

A map records distance, not the moment a prejudice begins to loosen

Before visiting a place, we already believe we know it.

That knowledge comes from news, books, films, social media, and other people's stories. The north must be one way and the south another. Large cities mean efficiency; smaller ones mean slowness. Life abroad is more open; life at home is more convenient. Famous universities are filled with geniuses. Popular landmarks must be worth reaching.

These impressions are not always false. They are simply too neat. The real world is rarely so orderly.

Moving between cities in China taught me that the same human need can have completely different local solutions. A city's character lives not only in its landmarks but in the speed of its morning commute, the distance between neighborhood shops, the way strangers speak to one another, and the number of steps required to get one small thing done.

Some places felt distant in my imagination and familiar upon arrival. Others looked dazzling in photographs but created little connection in person. I began to understand that our judgment of a place often reveals the boundary of our own experience more than it describes the place itself.

My international travel routes over the years

Crossing borders first changed my sense of scale

When I first traveled farther away, comparison came naturally. I compared roads, buildings, universities, companies, and services. Which were more advanced? Which were more convenient? Which should be studied and copied?

Later I realized that comparison alone remains a tourist's perspective. It compresses a complex society into a ranking and ignores the history, cost, and choices behind every form of order.

Standing on the UCLA campus, I naturally noticed its architecture, laboratories, and academic environment. But the more important question was not how famous the university was. I wondered how an environment gives young people permission to ask questions, enables people from different backgrounds to collaborate, and turns knowledge into long-term accumulation.

That observation revised my understanding of technology and innovation. Innovation is not a cluster of impressive buildings or a collection of fashionable concepts. It requires room for failure, durable institutions, open exchange, and ordinary people willing to work on one thing for many years.

At UCLA, what matters beyond the landmark is how knowledge is allowed to grow

As my world grew larger, I became slower to make conclusions

Travel did not make me “well-traveled,” at least not in the way I want to describe myself. Its clearest effect was to make me suspicious of certainty.

In engineering, we abstract, model, and generalize. These abilities help me work with complex systems, but they also create an illusion: gather enough information and a subject can be understood quickly.

Travel keeps reminding me that a person's life is not an interface, a city is not a set of metrics, and a culture is not a configuration file summarized by a few keywords. Some things appear only when we move closer. Even then, we see only a portion.

So when I encounter an unfamiliar industry, a new technical direction, or someone whose experience is far from mine, I try to ask one more question: Am I looking at reality, or at the map I have projected onto it?

That hesitation is not indecision. It prevents decisiveness built on a false premise.

Who travels with me changes the memory more than the destination

Years later, we often forget how many attractions we visited in a day. We remember the wind beside the sea, the warmth of leaning against someone, and a sentence spoken at that moment.

Travel temporarily removes us from familiar roles. Daily life surrounds us with work, family responsibilities, and schedules. In an unfamiliar place, navigation fails, weather changes, language may not work, and plans are rearranged. Inside these small uncertainties, I meet my companion again—and I meet another version of myself.

I see when I become anxious, when I become stubborn, and when I am willing to abandon a plan. I also see that a good relationship is not one in which two people always agree. It is one in which, when reality departs from expectation, both are still willing to find another route together.

What remains years later is often not the attraction, but the moment experienced together

I no longer define travel by having “been there”

“Been there” is too light a phrase. It may mean only that a body briefly occupied a coordinate, took a photograph, and left.

For me, the measure of a journey is what changes after I return:

  • Did it overturn a judgment I had treated as certain?
  • Did it make me less arrogant toward another way of living?
  • Did it renew my appreciation for conveniences I had stopped noticing?
  • Did it change how I build products, lead teams, or relate to people?
  • Did it make me admit that the world is more complex than my experience?

If nothing changes, even a long journey may be only movement.

The reverse is also true. We do not always need to travel far. Walking attentively through an unfamiliar neighborhood, talking deeply with someone from a different background, or staying in one place long enough can also force imagination to meet reality.

The next journey is not about filling the map

I will keep traveling. I will still take photographs, save routes, and keep those numbers. There is nothing wrong with records, and landmarks are not meaningless. The mistake is to confuse a record with a gain, or arrival with understanding.

Every line on a map appears to connect one city to another. The more important movement happens inside a person: certainty loosens, unfamiliar lives become understandable, and a narrow sense of scale opens again.

So before my next departure, I do not want to ask only which place is still missing from my map.

I want to ask: What am I willing to let reality correct this time?

The world does not need to be completed by me. It asks only to be encountered again, carefully and honestly.

09 / 41

Beyond the Model Race: How I Read the Cross-Pacific AI Chessboard

A systems view of Dimension Capital's China AI memo: how compute scarcity, engineering efficiency, open weights, energy, commercialization, and capital shape the U.S.–China AI landscape.
Sep 01, 2026 · 13 min read

I recently read a letter from Dimension Capital to its limited partners titled The Cross-Pacific AI Chessboard.

It appears to be an essay about U.S.–China AI competition. Its real value, however, is not another prediction about which country will win or another ranking of frontier models. It decomposes AI competition into a coupled system: models, chips, power, engineering efficiency, open weights, data, inference, commercial revenue, and capital markets.

That is much closer to how I understand the AI industry.

For the past two years, we have often watched one simple coordinate: who scores higher, who trains the larger model, and who releases the next capability first. A model is only the most visible layer. Whether that capability can be trained, deployed, adopted in production, monetized, and converted into the next round of compute depends on whether the whole loop closes.

My central conclusion is:

U.S.–China AI competition is no longer a model race. It is a competition between two production systems shaped by different forms of scarcity.

A systems map of the cross-Pacific AI landscape

Scarcity changes technical culture

Dimension's first argument is that scarcity drives evolution. Export controls reduced Chinese labs' access to advanced compute and pushed optimization deeper into the stack: from model architecture into optimizers, compilers, kernels, PTX, serving systems, and even chips and interconnects.

I agree with the observation, but I would phrase the mechanism more carefully. Scarcity does not automatically create innovation; it can also stop a team. Change happens when the target cannot be reduced and resources cannot grow linearly, forcing an engineering organization to redefine what is worth optimizing.

In a compute-rich environment, adding GPUs may produce the highest marginal return. In a constrained environment, removing a memory transfer, raising utilization, or cutting inference cost may matter more than scaling the model.

Over time, the default actions diverge:

relative abundance → scale the system → push the capability frontier
relative scarcity  → extract efficiency → maximize output per unit of compute

This is not a difference in intelligence. It is a difference in objective functions. Technical culture is often created by the bottleneck that actually blocks delivery every quarter.

China is gaining workload without equivalent revenue

The most memorable line in the memo is that Chinese models are gaining Western workload, but not equivalent Western revenue.

Open weights change the path into production. Traditional enterprise software requires vendor review, procurement, data agreements, and long-term service commitments. A model that can be downloaded, fine-tuned, and deployed on a company's own infrastructure may be adopted by engineers before it becomes an official dependency.

That helps explain why Qwen, DeepSeek, Kimi, and GLM can quickly enter overseas developer tools, agents, and inference platforms. What spreads is capability, not a SaaS seat.

Workload, however, is not revenue. Value may stay with several other layers:

  • the cloud selling GPUs or inference tokens;
  • the application company wrapping the model in a workflow;
  • the platform owning enterprise distribution;
  • the data, evaluation, and verification infrastructure;
  • and only then, perhaps, the model developer.

Open weights can compress the price of the model layer without compressing the value of the entire application chain. They are more likely to redistribute it: model capability becomes commoditized while deployment, distribution, proprietary data, and business loops gain bargaining power.

“Chinese models are widely used” and “Chinese model companies have strong commercialization” are therefore separate claims. Technical penetration is not a substitute for revenue quality.

Capability can cross the Pacific twice

Dimension describes a striking loop:

U.S. frontier model
  → distillation and engineering in a Chinese lab
  → open-weight model
  → adoption by a U.S. application company
  → sale to U.S. enterprise customers

Add data, GPUs, and overseas data centers, and the chain becomes even more complex. A Chinese team may rent NVIDIA compute in Southeast Asia, absorb capabilities from a U.S. frontier system, release open weights through global inference providers, and ultimately enter another American company's production stack.

The cross-Pacific circulation of AI capability

This makes “American model” and “Chinese model” increasingly coarse labels. The research organization has a nationality; the actual capability chain crosses hardware, data, training, distillation, deployment, and application markets.

Policy can change cost and speed, but cutting one edge rarely stops a network. Complex systems search for substitutes: remote compute, third-country data centers, open weights, model routing, distillation, synthetic data, and new suppliers reconnect broken paths.

The important question is no longer whether a connection exists. It is who pays for the connection, where revenue settles, and who controls the nodes with bargaining power.

The United States is constrained by power; China by chips

The memo compresses the opposing constraints into a useful contrast: the United States has greater access to advanced chips but faces increasing limits in power, data-center space, and grid construction. China can expand power infrastructure faster but remains constrained by advanced GPUs, manufacturing yield, and packaging.

This is more useful than asking which side has more compute, because compute is not one variable:

effective compute = chips × power × network × data center × software efficiency × uptime

If any factor approaches zero, expensive accelerators become underused inventory. Abundant electricity does not automatically become training capacity either; it still requires chips, interconnects, a software stack, and an engineering organization.

Both systems are trying to substitute around their shortage. The United States uses energy contracts, dedicated data centers, and more efficient models to soften its power constraint. China uses domestic accelerators, cluster engineering, compression, and an open ecosystem to soften the advanced-GPU constraint.

The decisive variable is not simply the size of an advantage. It is how quickly each system can reduce the force of its scarcest input.

Capability and revenue are not moving on the same curve

Dimension also argues that the commercial revenue gap is much larger than the model-capability gap. The memo cites annualized revenue estimates for OpenAI, Anthropic, and Chinese model companies. These numbers come from private-market reporting and industry conversations with inconsistent definitions, so I treat them as directional signals rather than audited statements.

The structural difference is visible without relying on an exact number.

Leading U.S. labs operate in a more mature market for enterprise software, APIs, developer distribution, and capital. Revenue buys compute, talent, and data-center commitments, which support the next generation of models. China has enormous user reach and rapidly diffusing open models, but consumer willingness to pay, enterprise contract sizes, and value capture at the model layer remain weaker.

This produces two different loops:

high revenue → more compute → stronger models → higher-value products → high revenue

open weights → more adoption → more feedback and ecosystem → lower model price → more adoption

The first loop concentrates capital and pushes the capability ceiling. The second diffuses capability and lowers adoption barriers. The future depends on whether each can solve its weakness: whether closed, high-value models can cover rising infrastructure costs, and whether an open ecosystem can convert usage into sustainable revenue.

The real competition is between incentive systems

Put DeepSeek, Qwen, Kimi, Doubao, and GLM in one frame and the intensity of competition inside China may be more immediate than the abstraction of national competition. Teams face product cadence, inference cost, model rankings, user growth, and financing before they face a grand geopolitical narrative.

American labs are shaped by their own incentives as well: API revenue, enterprise customers, capital expenditure, compute contracts, and safety commitments influence whether models are opened, when capabilities ship, and how access is priced.

States define constraints. Companies and research organizations optimize within them. The resulting ecosystem often emerges from accumulated locally rational decisions, not a centrally designed blueprint.

That is why I do not think of this as two players on a chessboard. It looks more like two deeply connected markets with intense internal competition and mutual dependence through models, hardware, data, and revenue.

The important part of RSI is verification

The later part of the memo discusses a weaker form of recursive self-improvement: models participating in their own research, coding, experimentation, and iteration.

I prefer to call this compression of the R&D loop rather than immediately invoking the stronger imagination attached to RSI. Models can already generate experiment code, analyze failures, propose candidates, assist data selection, and automate evaluation. They still depend on humans to define objectives, design verification, and bear the cost of error.

The scarce resource may not be compute alone. It may be high-quality verification:

  • Is the result actually better than the baseline?
  • Did the gain come from data leakage?
  • Does the capability reproduce on real work?
  • Did safety, cost, or latency deteriorate?
  • Who decides that an automated experiment deserves another round?

If self-improvement is mainly compute-bound, the U.S. advantage looks stronger. If it is verification-, engineering-, or experiment-throughput-bound, China's efficiency culture and open collaboration may matter more.

Models can produce answers faster. The system must become better at deciding which answers are true.

What I took from the memo

First, scarcity determines the direction of optimization. Shortages of chips, power, revenue, and verification capacity create different R&D cultures.

Second, capability flow has separated from value capture. A model from one country can serve another country's workload while revenue remains in cloud, inference, applications, and distribution.

Third, system competition cannot be explained by one metric. Model rankings, GPU counts, generation capacity, users, and valuations are each only one layer. What matters is whether they form a sustainable feedback loop.

Dimension's letter has an explicit investment perspective, and some claims about penetration, revenue, and model provenance need independent verification. Its analytical framework is still worth keeping: the U.S. and Chinese AI systems are deeply entangled, and competition no longer runs along one straight line.

I do not expect a simple winner. Model capability will probably continue to cross borders; infrastructure will become more regional; open and closed systems will coexist; and value will keep shifting among models, compute, data, applications, and distribution.

The signal worth watching is not who leads one benchmark. It is who turns a scarce input into an engineering capability that others cannot easily reproduce.

References

10 / 41

From AST to Bytecode: Making HHY's Default Engine 2.7× Faster

How I built HHY v1.3.10's Bytecode VM—from the AST semantic baseline, Compiler, and Verifier to Stream Kernels, profiling, cache governance, and a six-runtime performance comparison.
Sep 01, 2026 · 14 min read

A few days ago, I published Optimizing HHY's AST Interpreter Without Switching to Bytecode. My position was deliberate: first remove repeated work with a resolver, static slots, lightweight call frames, and escape-safe reuse. I did not want to rewrite the runtime simply because a language implementation was expected to have a VM.

HHY v1.3.10 now executes Bytecode by default.

This is not a reversal. It is the next step in the same engineering rule: introduce a new execution layer only when profiles and fixed workloads show that AST dispatch has become a cost worth addressing—and keep the AST as the semantic authority afterward.

The final CI evidence says that Bytecode's wall-time ratio to AST is 0.3695 on the 1M Core Flow CPU workload, making it about 2.71× faster. The ratio is 1.0088 for a short task, effectively even, and 1.0207 for a sustained JSON workload, about 2.1% slower. Those differences matter more than a blanket claim that bytecode is faster: they show that the bottleneck changes with the workload.

HHY execution architecture from the AST semantic baseline to the default Bytecode engine

I changed the AST's responsibility; I did not delete it

The HHY front end did not split when the VM arrived. Both execution paths share the same lexer, parser, checker, and resolver:

UTF-8 Source
  → Lexer / Parser
  → Checker / Resolver
  ├→ AST Evaluator
  └→ Bytecode Compiler → Verifier → Execution Plan → VM

The complete AST evaluator remains available explicitly:

hhy run --engine ast script.hhy
HHY_ENGINE=ast hhy run script.hhy

I treat it as a permanent semantic oracle, not legacy code waiting to be removed. Every complete fixture and practical project runs independently under AST and Bytecode, comparing stdout, stderr, exit status, diagnostics, resource limits, and cancellation behavior.

Maintaining two paths has a cost, but it gives optimization a precise boundary. If they disagree, I start by treating it as a VM bug rather than reinterpreting the language specification. The optimizer does not get to define semantics.

Bytecode is not the AST under a new name

HHY's Bytecode Compiler lowers every AST node into an explicit opcode while preserving its source location, child and subtree shape, constant reference, and the local-slot metadata already produced by the resolver.

hhy bytecode examples/00-hello.hhy

This command compiles, verifies, and disassembles without executing the result. Disassembly is not a showcase feature to me; it is a governance mechanism. When the runtime selects a path, I need to see exactly what the compiler emitted.

Bytecode converts tree-shaped dispatch into a denser, more sequential, pre-verifiable execution representation. The evaluator no longer needs to rediscover structure at each AST node; the VM follows prepared instructions and operand shapes.

It does not bypass the existing runtime. Closures, exceptions, GC, cancellation, streams, effects, and resource limits still have one owner in the shared runtime. The new layer changes how execution reaches an operation; it does not create a second Value, Stream, or Error model.

The Verifier was a prerequisite for making Bytecode the default

It is tempting to trust in-memory bytecode because the compiler that produced it lives in the same process. That assumption does not survive fuzzing, memory damage, or a possible future cache format.

An independent Verifier therefore follows the Compiler. It rejects:

  • unknown opcodes and invalid constant references;
  • malformed trees, excessive nesting, and non-canonical HALT;
  • out-of-range slot metadata;
  • invalid Stream Kernel type flow and stack shape;
  • instructions that follow a terminal RETURN.

Execution planning also proves bounded instruction, frame, and operand shapes before evaluation. The Runtime cannot bypass Compile, Verify, and Prepare. This internal boundary is independently versioned and protected by static governance tests.

I was willing to make Bytecode the default not when it first ran a benchmark, but when malformed inputs failed closed, the AST rollback remained available, and both engines had continuous differential evidence.

From general Bytecode to Stream Kernels

HHY is a Flow-first systems scripting language. Optimizing general expression dispatch while ignoring common Stream composition would leave much of the opportunity untouched.

Before v1.3.7, the Runtime already contained a small Stream Int fast path, but its selection depended on magic kinds that were difficult to explain and verify. Subsequent releases tightened the design step by step:

  1. v1.3.7 replaced magic kinds with named operations, centralized metadata, and stable fallback reasons;
  2. v1.3.8 moved expression-shape recognition from the Runtime into the Compiler and emitted versioned Stream Kernel IR;
  3. an independent Verifier checked opcodes, type flow, stack shape, constants, and RETURN;
  4. the Runtime executed only verified kernels, while unknown or dynamic closures fell back losslessly to general Bytecode;
  5. v1.3.9 made ordinary execution and profiling use the exact same optimization decision.

Fallback is not a performance failure in this design. It is a correctness mechanism. The Compiler specializes only what it can prove; general Bytecode preserves complete semantics everywhere else.

Performance: where it wins and where it does not

The final release-candidate gate used paired, interleaved fresh-process wall-time samples, executing the same source under AST and Bytecode.

WorkloadBytecode / ASTInterpretation
Core Flow 1M0.3695Bytecode is about 2.71× faster
Basic Flow short task1.0088Essentially even; Bytecode is about 0.9% slower
Sustained JSON Flow1.0207Bytecode is about 2.1% slower

Core Flow generates 0..999999, multiplies each value by two, retains values divisible by three, applies stable distinct, materializes, and counts. It magnifies expression dispatch and Stream processing, which is where Bytecode and verified kernels deliver a clear gain.

A short task is dominated by process startup and fixed initialization, leaving little for the execution engine to change. The JSON workload includes parsing, maps, and memory operations, where Bytecode currently shows no advantage. I kept both results visible because they tell me that the next phase should come from profiles rather than from adding opcodes indiscriminately.

Profiling itself is gated. Final overhead was 1.0269×, or about 2.786 ms, below the 1.35× and 12 ms limits. Profile JSON schema 2 records selection reasons, Kernel versions, and actual opcodes, then attributes CPU and Heap hotspots to the path that really executed.

A same-machine comparison with five other runtimes

To place the result in a more intuitive context, I compared Go, Lua, PHP, Java, HHY, and Python on the same 1M CPU/materialization workload on macOS 26.6.2 arm64. Every implementation returned 333334. Each of two rounds used two warmups followed by seven timed fresh processes per runtime in a deterministic shuffled order. Go and Java compilation was excluded; runtime startup was included.

Two-round median performance of six runtimes on the same 1M Flow workload

RuntimeRound 1 medianRound 2 median
Go 1.27.07.995 ms7.969 ms
Lua 5.5.118.368 ms17.957 ms
PHP 8.5.1043.174 ms43.549 ms
Java 26.0.2.149.394 ms48.153 ms
HHY 1.3.1055.297 ms53.404 ms
Python 3.14.781.747 ms86.459 ms

On this workload, HHY is faster than Python and near PHP and Java, while remaining materially slower than Lua and Go. This is not a language ranking, and it says nothing general about file, HTTP, process, or crawler workloads. It shows only that, on a fixed CPU and materialization task, HHY's default Bytecode Runtime has entered a comparable range while retaining obvious room to improve.

I care more about publishing the method and all 84 samples than selecting the best single number for a headline.

Why I did not add a Bytecode cache

Once bytecode exists, .hhyc, an in-process cache, or externally precompiled artifacts look like the natural next feature. A cache, however, brings invalidation, compatibility, security, and atomic-write requirements. Completeness is not evidence of need.

v1.3.10 measured before deciding. Five fixed real workloads used 21 paired fresh processes each. Compile-and-verify medians were only 0.004–0.012 ms, or 0.0078%–0.1341% of cold Bytecode wall time. They reached neither side of the joint admission threshold: at least 1 ms and at least 20% of cold wall time.

HHY therefore currently has no:

  • in-process Bytecode cache;
  • disk .hhyc cache;
  • external precompiled-Bytecode loader;
  • public Bytecode ABI.

This is not a missing checkbox. It avoids trading almost invisible savings for a large compatibility and attack surface. If future data crosses the threshold, a cache must bind source and recursive dependency hashes, HHY / Bytecode / Kernel versions, compiler features, the target, and security policy. Even a hit must pass checksum validation, bounded decoding, the full Verifier, and execution-plan verification.

From “not yet” to default Bytecode

Looking back, the meaningful question was never whether AST or Bytecode was more advanced. It was whether each step answered the most real problem at the time.

In v1.1.1, the major costs were repeated name lookup, general environment allocation, and frame lifetime. The resolver and slots produced about a 3.39× improvement on the same Fibonacci(30) benchmark. That work also prepared stable slot and scope metadata for later lowering.

Once the repeated semantic work was removed, profiling exposed the remaining dispatch cost more clearly. Bytecode then became a measured engineering choice instead of an architectural wish.

My conclusion has not changed:

Turn semantic proof into a shorter path first, then let data decide whether a new execution layer is justified.

HHY v1.3.10 defaults to Compiler- and Verifier-checked Bytecode, with the AST permanently retained as the semantic oracle and emergency fallback. CPU-heavy flows already gain substantially; short tasks and JSON paths still tell me to stay disciplined.

A VM is not finished when it can execute instructions. It is finished when it can run faster while keeping every language promise explainable, reversible, and verifiable.

References

11 / 41

Entanglement, Black Holes, and Qi: Three Conjectures About the Universe and Us

From quantum entanglement and Webb's early-universe surprises to black-hole cosmology, computational reality, and why I believe qi names a genuine dimension of lived experience.
Aug 30, 2026 · 11 min read

What fascinates me about science is not only that it keeps producing answers. Just when we think the world has been explained, science opens a larger crack in the picture.

Quantum entanglement tells us that separation does not necessarily mean independence. Black-hole physics makes us rethink spacetime in terms of information, entropy, and computation. The James Webb Space Telescope keeps placing objects before us that seem to have appeared too early in cosmic history.

None of this proves that people share a quantum connection. It does not prove that we inhabit a virtual world, and it does not place qi inside established physics. Still, it makes me take three conjectures seriously:

  1. A human bond may resemble entanglement: once something real has happened between two people, distance alone cannot erase it.
  2. Reality may fundamentally be an information-processing environment, with black holes as its most extreme quantum information systems.
  3. What Chinese tradition calls zhenqi, or vital qi, may name a genuine state created by the body, breath, attention, and consciousness—even if we lack a precise scientific language for it.

A cosmic image of entanglement, a black-hole universe, and vital energy

This essay is not a scientific conclusion. It is a personal reflection written at the boundary of what science currently knows.

Entanglement: distance cannot undo an established relationship

Quantum entanglement is often paraphrased carelessly: no matter how far two particles are separated, changing one makes the other instantly “feel” it and receive a message.

The more accurate account is that an entangled pair must be described as one quantum system. Measurements made far apart produce correlations that classical local theories cannot explain. The Bell-inequality experiments recognized by the 2022 Nobel Prize in Physics established that these correlations cannot be reduced to a simple theory of local hidden variables.

Entanglement does not, however, let us send a controllable message faster than light. Each observer sees random results locally. The correlation becomes visible only after the observers compare their data through an ordinary communication channel.

That distinction matters. Physical entanglement is not telepathy, and it does not prove destiny between people.

Yet the metaphor still moves me.

When two people truly meet, they create a shared history that cannot be reduced to two isolated individuals. A sentence, an act of trust, or a period of working side by side changes how both people later see the world. They may become distant and rarely speak, but the shared experience has already entered their memories, choices, and character.

For me, destiny is not a mysterious force transmitting signals across space. It is something simpler and deeper:

Once two people have genuinely participated in each other's lives, they have changed each other's initial conditions. Distance can interrupt communication, but it cannot remove an event from history.

I use entanglement as a metaphor for human connection not because the two are physically identical, but because it reminds me that isolated objects may not be the only fundamental ingredients of reality. Relationships can be real too.

Webb is showing us a universe that matured too soon

The familiar story of cosmic evolution runs roughly like this: after the big bang, matter cooled; the first stars and small galaxies formed; structures merged and accreted over long periods; only then did the enormous galaxies and supermassive black holes of the later universe emerge.

Webb has not demolished that story, but it has made the story much less comfortable.

It has found abundant, bright, and compact objects only a few hundred million years after the big bang. Some early galaxies appear brighter, more massive, or more structurally developed than earlier models led us to expect. Some black holes appear too early and grow too quickly. The problem may not be the hot early phase described by big-bang cosmology. It may be our incomplete account of how the first galaxies and black holes formed after it.

The “Little Red Dots” are among the most striking puzzles. They are abundant during roughly the first 1.5 billion years of cosmic history. Interpretations have ranged from compact young galaxies and dust to active black holes. New deep spectroscopy now points some of them toward a stranger picture: a supermassive black hole wrapped in a hot, dense, partially ionized gas cocoon—the “black hole star” model.

In 2026, Webb identified more than forty spectral lines in GLIMPSE-17775. Multiple independent features support the interpretation of a rapidly accreting black hole hidden inside dense gas. Research on another Little Red Dot, Abell2744-QSO1, adds evidence that some supermassive black holes may have become enormous before a mature host galaxy existed around them.

What these observations challenge most directly may be the sequence we took for granted:

old intuition:
small galaxy → stellar death → small black holes → slow growth into a supermassive black hole

new possibility:
direct collapse of dense gas → massive black-hole seed → black hole and galaxy co-evolve

Webb has pushed our sight closer to cosmic birth. It is not showing a standard model overturned overnight. It is making the blank spaces in that model begin to glow.

Could we live inside a black hole?

Black-hole cosmology proposes a bold possibility: the interior of a black hole may not terminate in a physically infinite singularity. In models involving torsion, quantum gravity, or other high-density effects, collapsing matter could bounce at an extreme but finite density and create a new expanding spacetime.

If such a model is correct, an observer in a parent universe sees matter collapse into a black hole. An observer on the inside may instead experience something like the birth and expansion of a universe.

parent universe
  ↓ gravitational collapse
black hole and event horizon
  ↓ singularity avoided by a high-density bounce (hypothesis)
expanding child universe
  ↓ stars and new black holes form
more child universes (further conjecture)

The event horizon causally separates the two regions. Even if we lived inside a black hole, we would not expect to look outward and see the parent universe. From outside, the object is a black hole. From within, space might be vast and continue expanding.

What attracts me to this theory is the way it rewrites the big bang from creation out of nothing into inheritance. Our universe may not be an absolute beginning. It might be the interior continuation of a collapse elsewhere. Universes produce black holes; black holes may produce universes; reality becomes a branching lineage.

The boundary must be explicit: there is currently no observational proof that our universe is inside a black hole. Webb's early black holes and Little Red Dots do not directly confirm black-hole cosmology. The defensible connection is that new anomalies can make neglected theories worth discussing again—not that the anomalies have proved them.

Is a black hole a quantum computer?

If computation means physical states evolving by rules and thereby processing information, a black hole looks like one of nature's most extreme information systems.

Black-hole entropy is related to event-horizon area. How information that falls in is preserved, scrambled, and perhaps returned through Hawking radiation remains one of the deepest questions in physics. Quantum-information researchers describe black holes as extraordinarily fast scramblers and study them through the language of quantum circuits, entanglement, and error correction.

So when I say “a black hole is a quantum computer,” I do not mean that it contains chips, software, and a screen. I mean something more precise:

A black hole may compress matter, energy, spacetime, and quantum information into one physical process. It is not a manufactured computer, but it can be understood as a system that processes information under quantum rules.

One step further lies the question of whether the universe itself is a computation. Some theoretical programs treat spacetime as emerging from deeper quantum information processing. If space, time, and matter are all internal states of a system, what we call reality may indeed resemble an enormous runtime environment.

But “computational” does not mean “simulated by a programmer.” The simulation hypothesis has no decisive evidence, and it is difficult to formulate a test that distinguishes it from ordinary physical law.

When I call reality virtual, I do not mean that it is fake. I mean:

The reality we experience may be an interface presented to observers by deeper rules.

A table remains solid. Love remains real. Choices continue to have consequences. Even if reality is made of information, life loses none of its meaning. A character inside a game does not perceive the transistor, but its level still has stable rules. In the same way, our inability to see the lowest layer of reality does not make our own layer unreal.

Qi: perhaps not a particle, but a whole-body state

I believe that what people call zhenqi—vital qi—is real.

I do not need to claim that it is an undiscovered elementary particle. Nor do I think every unexplained sensation can be labeled “energy.”

During deep breathing, concentrated attention, relaxation, or sustained practice, people can genuinely experience warmth, flow, fullness, vibration, or a current moving through the body. Modern physiology can approach these experiences from several directions: breathing changes autonomic regulation, posture changes muscle tension and circulation, attention amplifies interoception, and training improves our ability to discriminate internal signals.

Those explanations do not erase the experience. They give it a bodily foundation.

I prefer to understand qi as an old, compressed name for a complex system state:

breathing rhythm
+ nervous-system state
+ circulation and muscle tension
+ interoception
+ attention and emotion
+ learned movement patterns
= the whole lived state experienced as “qi”

Modern science tends to divide a system into variables. Traditional practice often begins with the whole sensation. The two languages may point toward the same embodied person from different directions.

Current evidence does not establish a separate qi energy that can be independently measured, detached from these physiological processes, and transmitted between bodies. I have to acknowledge that boundary. But I am equally unwilling to declare a lived experience worthless simply because it does not yet have a dedicated instrument reading.

For me, qi is first a trainable and perceptible dimension of living that changes the quality of action. Future science may decompose much of it into couplings among nerves, breathing, fascia, circulation, and consciousness. Some of it may remain part of the first-person language of being alive.

The three conjectures ask one question

Entanglement asks whether relationship can be more fundamental than distance.

Black-hole cosmology asks whether an ending can also be another beginning.

Qi asks whether life can be fully explained after it has been separated into parts.

These ideas operate at entirely different scales and have no demonstrated causal link. I place them together not to invent a “quantum mystical theory of everything,” but because each challenges an overly simple intuition: that individuals are isolated, that the universe has only one absolute beginning, and that a body is nothing more than the sum of its components.

My view can be condensed into three statements:

Destiny is not quantum entanglement, but entanglement changes how I understand relationship.

Black holes have not been shown to give birth to universes, but they offer another way to imagine what came before our big bang.

Qi has not been established as an independent physical energy, but the human experience of inner vitality is real, worthy of study, and worthy of respect.

The purpose of science is not to stamp every unknown as superstition. The purpose of imagination is not to package every hope as “scientifically proven.” The difficult and fascinating place is between them.

I want to preserve doubt, and I want to preserve wonder.

Perhaps we will one day discover that cosmos, life, and consciousness are different-scale expressions of one immense information process. Perhaps we will not.

As long as we keep observing carefully, distinguish evidence from belief honestly, and learn to ask better questions, the exploration itself already has meaning.

Further reading

12 / 41

Do Not Build Only One Career: Opportunity Windows, Second Curves, and Continuous Evolution

A career should capture value when personal strengths and market tailwinds overlap, build a second curve before the first one breaks, and preserve choice through continuous learning.
Aug 28, 2026 · 7 min read

After years of working in technology, I increasingly believe that a career cannot be explained by a simple choice between stability and job-hopping.

What a person earns depends on capability, but also on industry cycles, location, platform, team, and stage of life. Effort matters. The time and place in which that effort is applied matter just as much.

I have condensed my thinking into three principles: capture value during your window of opportunity, design a second curve before you need it, and keep learning how the world is changing.

Career opportunity, a second curve, and continuous evolution create long-term optionality

Capture your value when timing, position, and people align

Every industry has a window. Every person has one too.

During the rapid expansion of the mobile internet, software engineers were scarce. For many people, changing companies roughly every two years was an effective way to increase compensation, responsibility, and technical exposure. The value did not come from changing jobs for its own sake. It came from a market willing to pay more for a scarce capability.

There is no universal formula, however.

If you are inside an exceptional company whose business is growing, whose team deserves trust, and where you can take on greater responsibility and share the upside through equity, long-term incentives, or organizational growth, staying may create far more value than frequent moves. During Tencent's high-growth years, some people who stayed and grew with the company earned returns that salary alone could never have produced.

The opposite is also true. If a small or medium-sized company has limited growth, management continuously drains people, and the owner demands loyalty while refusing to share results, “stability” may simply mean selling your most valuable years too cheaply.

My principle is straightforward:

Within the boundaries of law, professional ethics, and long-term credibility, choose the path that best fits you and maximizes your long-term interests.

Those interests include more than this year's salary. They include capability, industry position, health, family responsibilities, transferable relationships, visible work, and future options.

Do not switch jobs mechanically for a small raise. Do not stay indefinitely because of habit, fear, or a founder's distant promise. Ask instead: Is this platform still amplifying me, or has it begun to consume me? Am I sharing in the growth, or only carrying its cost?

Build the second curve before the first one fails

In a rising economy, people easily overestimate the stability of a job. In a downturn, organizations recalculate costs with little sentiment.

Software engineers are particularly vulnerable to one illusion: if the technical work remains solid and delivery continues inside one company, the career will naturally continue. Yet a title, internal authority, and recognition often belong partly to the organization. When the role disappears, they do not always convert directly into value in the external market.

If an experienced engineer waits until a layoff to update a résumé, study the market, learn new technology, and search for a new professional identity, every source of pressure arrives at once. The anxiety is not only about being between jobs. It comes from having one income curve, one professional identity, and one way for the market to price you.

A second curve should therefore not be an emergency response. It should begin while the primary career is still healthy.

It does not necessarily mean quitting to start a company, and it is not the same as finding a random side hustle. A second curve might mean:

  • developing AI, product, data, or management capability beyond the primary stack;
  • writing, contributing to open source, and turning experience into visible work;
  • joining real projects that create a delivery record independent of the current employer;
  • building a small loop around consulting, teaching, tools, content, or a product;
  • maintaining trusted relationships across companies, industries, and age groups;
  • becoming independently known for solving a specific class of problems.

The second curve is usually weak at first. Its immediate return may be far below the main job. Its purpose is to stop capability, reputation, and income from being completely tied to one organization.

The first curve provides today's certainty. The second curve protects tomorrow's options.

When conditions change, a person with a second curve may still feel anxious, but does not start from zero. There is visible work, market feedback, a different combination of capabilities, and an understanding of how the world outside the current company assigns value.

Use weekends to evolve, not to exhaust yourself

I believe in learning during weekends. I do not believe every hour of rest should become a second shift.

Continuous evolution is not an occasional all-night sprint through a new framework. It is a sustainable rhythm: reserving a few hours each week to read, practice, write, talk with people, or build something small and real.

This matters especially now. AI is changing how knowledge is acquired, code is written, products are built, and organizations collaborate. Much of our experience remains valuable, but experience that cannot combine with new tools and contexts can harden into a constraint.

Beyond technology, I also try to understand what younger people think, what they use, and why they make different choices.

The goal is not to imitate them or chase every trend. It is to avoid explaining the present only through the experience that made us successful in the past. Younger people's media habits, tools, consumption patterns, expectations of work, and styles of expression often reveal new product opportunities and organizational shifts early.

Remaining young in a useful sense is not about adopting fashionable language. It is about staying curious about unfamiliar things and accepting that one's knowledge structure must continue to change.

Treat a career as a system you actively design

Career security is not the same as having a long-term contract or spending enough years in one company.

More durable security comes from capabilities that work across environments: solving real problems, making outcomes visible to the market, recombining resources after a platform changes, carrying enough buffer through an income interruption, and continuing to collaborate with people from different generations and backgrounds.

If I treat a career as a system, I keep returning to four questions:

  1. Window: Is my industry and role expanding, maturing, or contracting?
  2. Value: Does the current platform reward me in proportion to the responsibility and results I create?
  3. Curve: Beyond my current job, am I building another capability and identity that the market needs?
  4. Renewal: Am I still engaging with new tools, a new generation of users, and new ways of working?

These questions have no permanent answers. Life stages change, family responsibilities change, and industries move through cycles. The goal is not to make the correct choice every time. It is to avoid handing your ability to choose entirely to one company, one manager, or one skill.

Closing thought

Looking back, the greatest career regret is often not missing one salary increase. It is failing to assess your value during the years when your window was open. The deepest risk is not losing a job on one difficult day. It is making no preparation while everything still appears stable.

When it is time to advance, advance. When capability, market momentum, and personal circumstances align, ask for fair value. Do not use cheap versions of “loyalty” to comfort yourself.

When it is time to accumulate, be patient. A second curve rarely produces impressive returns at the beginning. Over time, however, it turns anxiety into preparation and passive waiting into active choice.

Learning connects both curves. Weekend reading, practice, building, and conversation are not meant to keep us permanently busy. They help us understand a changing world, remain able to participate in it, and retain the capacity to begin again.

The best career strategy is not predicting which company will remain safe forever. It is building the ability to choose again at every stage.

13 / 41

Building a Multi-API Data Collector: Three Sources, One Incremental Table

How I used HHY to collect OpenAlex, Crossref, and GitHub pages concurrently, normalize semantics, isolate failures, and incrementally merge CSV.
Aug 27, 2026 · 7 min read

Collecting one API is easy. The engineering question begins when three sources disagree about pagination, JSON structure, and metrics, yet must enter one reliable flow—and a second run must update rather than blindly replace the first.

This project collects OpenAlex works, Crossref metadata, and GitHub repositories. By default it requests two pages of five records from each source and normalizes them into seven columns: source, external_id, title, url, record_type, metric_name, and metric_value.

The actual Multi-API Data Collector layout

Make pagination differences explicit jobs

OpenAlex and GitHub use pages while Crossref uses offsets. jobs.hhy represents that difference instead of hiding it:

for page in range(1, config.pages + 1) {
    jobs = append(jobs, { source: "OpenAlex", page: page, offset: 0 })
    jobs = append(jobs, { source: "Crossref", page: page, offset: (page - 1) * config.per_page })
    jobs = append(jobs, { source: "GitHub", page: page, offset: 0 })
}

Six jobs enter parallel(3). Each waits 250 ms before its request and uses a ten-second timeout, two retries, and 500 ms backoff. This is not a precise global rate limiter, but it prevents the example from starting with a burst against public APIs.

Unauthenticated GitHub search has a low quota. A production flow should reduce frequency or inject authenticated requests through a safe process extension, not store a token in project configuration.

Normalize shape without pretending semantics match

Each API has separate request and normalization functions. OpenAlex reports citations, Crossref references, and GitHub stars. They share the metric_name/metric_value shape without being collapsed into an ambiguous score.

OpenAlex → work id / display_name / citations
Crossref → DOI / title / references
GitHub   → full_name / repository / stars
                    ↓
source / external_id / title / url / type / metric_name / metric_value

A common schema simplifies storage, deduplication, and consumption. Retaining the metric name preserves source meaning.

Parsing can fail after a successful download, so network and normalization stages have separate attempt boundaries. Failures retain source, page, and error in failures.json. Successful pages can still merge, while the overall report becomes unsuccessful and the process returns 1.

Incremental merge requires stable identity

The collector loads existing CSV when present and flattens old plus new records into one Stream. Identity is source + external_id:

[existing, incoming]
    |> stream
    |> flat_map { records -> records |> stream }
    |> group_by { record -> join([record.source, record.external_id], ":") }
    |> map { group -> group.values[length(group.values) - 1] }
    |> sort_by { record -> join([record.source, record.external_id], ":") }
    |> collect

Old records come first and incoming records second, so the last value naturally replaces a matching key. Sorting by the same identity prevents API ordering changes from producing meaningless diffs.

CSV, the run report, and the failure list are all written atomically. Every record retains its source URL for verification.

Incremental verification across two collector runs

Why the test runs twice

The local fixture first returns twelve cross-page inputs that deduplicate to nine unique records. The second run loads those nine, collects again, replaces matching identities, and still ends with nine.

sh practical-projects/multi-api-data-collector/self-test.sh

One run proves that CSV export works. Two runs verify composite identity, replacement direction, stable ordering, and an idempotent shape. The fixture uses a random local port and temporary directory, then removes itself without contaminating production output.

The boundary of this flow

parallel improves HTTP waiting; it is not a claim about numeric throughput. retry handles transient failure; it cannot repair an incompatible schema. Incremental replacement preserves the latest observation; it is not full history.

With those boundaries explicit, the project shows HHY's intended direction: a small set of composable network, Stream, error, and atomic-file primitives that keep reliability policy inside the business flow.

References

14 / 41

Building Asset Governance: Audit Before Effects

How I used HHY to detect risky assets, emit a reviewable action plan, and apply allow-listed remediation through runtime-native dry-run semantics.
Aug 27, 2026 · 6 min read

The dangerous property of a cleanup script is often not that it cannot delete files, but that deletion is too easy.

I therefore split Asset Governance into two programs. audit.hhy observes and writes a report. cleanup.hhy executes only explicit copy, move, and remove actions from that report. Between judgment and effects sits a boundary that can be read, reviewed, and dry-run.

The closer automation gets to destructive effects, the more its plan should become a first-class artifact.

The actual Asset Governance layout and risky fixtures

Inventory before remediation

inventory uses files("**/*"), totals real Bytes, and checks three metadata rules: size beyond a configured threshold, uppercase letters or spaces in names, and modification time older than 365 days.

Content inspection reads only supported text extensions. A possible credential becomes a critical finding, but matching text never enters the report. Duplicate content is grouped by its text while the result retains only filenames.

Images and videos are deliberately not decoded. This project governs file metadata and placement. Detecting an oversized MP4 does not justify adding a media stack.

The report is evidence and input

The report contains inventory statistics, findings, and configured actions. A critical finding makes audit.hhy return 1, but the full report is still written atomically.

That differs from moving files inside the scanning loop. A failed audit is not uncontrolled execution; it is a successful discovery that requires attention, with evidence preserved for the operator.

The cleaner recognizes only three actions:

if action.kind == "copy" {
    copy(source, path_join(root, action.target), { overwrite: true })
} else if action.kind == "move" {
    move(source, path_join(root, action.target), { overwrite: true })
} else if action.kind == "remove" {
    remove(source)
} else {
    exit(3)
}

Actions are constrained by the report and never routed through a shell. Arbitrary commands and their injection surface are outside the model.

Dry-run belongs to the runtime

cleanup.hhy contains no hand-written if dry_run branch. The same program runs as:

hhy run --dry-run practical-projects/asset-governance/cleanup.hhy PROJECT report.json

The runtime EffectDispatcher intercepts copy, move, and remove operations and prints the plan. Business code follows the same control path without changing files. This is more dependable than duplicating preview logic at every effect call site.

Asset Governance audit, dry-run, and applied-remediation verification

The self-test creates an isolated mktemp workspace, verifies large, naming, stale, sensitive, and duplicate findings, proves that dry-run changes nothing, then applies and asserts copy, move, and remove.

The engineering principle I kept

Safe automation does not mean refusing every dangerous operation. It means separating observation, decision, and execution so every stage has reviewable inputs and outputs.

The most important result is the audit chain: file facts become findings; findings and configuration become actions; actions pass through dry-run before real effects. HHY's Path, Bytes, Duration, Regex, and Effect models keep that chain out of stringly typed shell assembly.

References

15 / 41

Building DataFlow ETL: CSV, JSON, and HTTP in One HHY Flow

How I built a complete HHY pipeline for CSV and JSON ingestion, concurrent profile enrichment, grouped reporting, and atomic dual output.
Aug 27, 2026 · 6 min read

ETL is a useful test of whether a language's data model survives beyond syntax examples.

A real pipeline rarely starts with one tidy JSON array. Inputs arrive as CSV, directories, and HTTP responses. Fields need normalization, records become ineligible, remote enrichment fails, and outputs must serve both humans and downstream programs.

DataFlow ETL reads a customer CSV and a directory of event JSON, normalizes fields, excludes inactive customers, enriches profiles concurrently, applies a minimum-spend rule, aggregates departments, and atomically writes JSON plus CSV.

The actual DataFlow ETL project layout

Turn each source into explicit data

CSV ingestion owns whitespace cleanup, lowercasing, boolean conversion, and integer parsing:

return path_join(input_root, "customers.csv")
    |> read_lines
    |> parse_csv({ header: true })
    |> map { row ->
    return {
        id: trim(row.id),
        email: lower(trim(row.email)),
        active: lower(trim(row.active)) == "true",
        spend: to_int(row.spend)
    }
}
    |> collect

Events take a different path through files("*.json"). I did not build a universal loader: the formats differ, but both source functions return Maps the business pipeline can understand.

Enrich concurrently without erasing failure

Inactive customers are filtered before HTTP work. Eligible customers enter parallel(4); each request has a three-second timeout and one retry.

let profile = attempt {
    http.get(join([api_base, "profiles", customer.id], "/"))
        |> timeout(3s)
        |> retry({ count: 1, backoff: 50ms })
        |> send
        |> response_body
        |> parse_json
}

A failed enrichment does not make the customer disappear. The output retains the original fields and adds enriched: false plus an error. The report is successful only when every qualified customer was enriched. That is more honest than silently dropping records and gives an operator something actionable to retry.

Streams express relationships; arrays mark boundaries

Qualification is a short pipeline:

customers
    |> stream
    |> where { customer -> customer.spend >= minimum_spend }
    |> sort_by({ order: "desc" }) { customer -> customer.spend }
    |> collect

Department totals use group_by and sum. I keep data in a Stream while it is being transformed, then collect where concurrency, reuse, or report ownership needs a concrete array. That makes lifetime visible and avoids consuming a single-pass Stream twice.

Two outputs, one business path

JSON retains customers, events, aggregates, and source statistics for diagnosis. CSV projects only seven stable downstream columns. Both writes are atomic, so a failed run does not leave half of a new file over the previous good result.

DataFlow ETL end-to-end verification

The deterministic test verifies three qualified customers, two event files, and two departments, along with email cleanup, spend ordering, remote region/tier values, and both output formats.

sh practical-projects/dataflow-etl/self-test.sh

The lesson from this flow

ETL complexity usually sits at boundaries rather than inside one map: when cleaning occurs, whether failures remain visible, how remote calls are bounded, and whether output replacement is safe.

That is also where HHY earns its place. Pipes keep direction clear, Streams express filtering and grouping, attempt turns failure into data, and the runtime provides shared timeout and atomic-effect semantics. The result is not merely SQL-like syntax. It is a pipeline that can run, fail, explain itself, and run again.

References

16 / 41

Building FlowGuard: A CI-Ready Quality Gate in HHY

How I composed filesystem checks, credential scanning, quality commands, HTTP health checks, and atomic reports into a verifiable HHY workflow.
Aug 27, 2026 · 7 min read

I have never found “it can print Hello World” to be convincing evidence that a language is useful.

For a systems scripting language such as HHY, the better question is whether it can carry a complete job involving files, subprocesses, HTTP, bounded concurrency, failure isolation, structured output, and stable exit codes. FlowGuard is my first practical answer.

It accepts a project directory, JSON configuration, and report path. It checks required files, scans file size and possible credentials, runs quality commands and service checks concurrently, then atomically writes JSON. No failures means exit code 0; a failed gate returns 1; invalid usage returns 3.

A quality gate is not a list of commands. It is a workflow that must finish collecting facts even when individual checks fail.

The actual FlowGuard project layout

Keep orchestration at the entry point

flowguard.hhy does not own every detail. Six modules separate structure, files, commands, health, shared checks, and reporting. The entry point composes them:

let structure_checks = inspect_structure(root, config.required_files)
let file_checks = inspect_files(root, config.limits.large_file)
let command_checks = inspect_commands(root, config.commands)
let health_checks = inspect_health(config.health_checks)
let checks = combine([structure_checks, file_checks, command_checks, health_checks])
let report = build_report(config.project.name, args[0], checks)

report
    |> encode_json({ pretty: true })
    |> save_text(output_path, { atomic: true })

This is the shape I want HHY to express: inputs, transformations, effects, and the result boundary remain visible. Modules exist so each class of check can evolve and be tested independently, not to turn a small program into ceremony.

attempt matters more than fail-fast

A gate that exits on the first failed command provides poor diagnostics. CI may report a failed test but never reach the missing license, unhealthy endpoint, or credential finding.

FlowGuard wraps each external command in attempt, converting execution failure into data. HTTP checks use the same model. A failure becomes a structured status: "fail" record instead of terminating the report.

let execution = attempt {
    run(command.argv, {
        cwd: root,
        timeout: 15s,
        max_output: 1mib
    })
}

Arguments go directly to run as an argv array, never through a constructed shell command. Every command has a fifteen-second timeout and a 1 MiB output ceiling. The useful property is not shorter syntax; it is that injection, runaway execution, and unbounded output are outside the default path.

Bound concurrency and redact findings

Quality commands use parallel(3) and health checks use parallel(4). These tasks mostly wait on processes or networks. Bounded concurrency reduces wall time without allowing a larger configuration to exhaust the machine.

The scanner recognizes patterns such as private-key headers, DEMO_TOKEN=, and password=, but stores only a filename and content_redacted: true. A tool may need to detect a secret; that does not give it permission to reproduce one.

File limits use real Bytes values. Configuration strings such as 256b, 1kib, 4kib, and 1mib are parsed before comparison, keeping units in the type and out of naming conventions.

A gate must test its failure path

The end-to-end test includes two deterministic repositories. healthy-project has required files, successful commands, and a 2xx health endpoint; all eight checks pass. risky-project deliberately omits LICENSE, includes a fake DEMO_TOKEN, runs a failing command, and requests a 404 endpoint.

Exit code 1 in the second scenario is a correct result. The harness then reads the report and verifies that all five failures exist and no matching credential content escaped.

FlowGuard end-to-end verification of healthy and risky scenarios

sh practical-projects/flowguard/self-test.sh

What the project proved

FlowGuard confirmed where HHY is most distinct. It is not trying to replace one shell command. It gives files, processes, HTTP, and streams one readable model for errors and resource limits.

Shell remains excellent for connecting a few happy-path commands. Once a job requires bounded concurrency, error aggregation, redacted reports, atomic writes, and stable exit codes, the script is becoming an application. HHY is meant to keep that transition direct.

References

17 / 41

Building HHY Collector Framework Without Starting from a Scrapy Clone

How I combined HHY HTTP, Streams, bounded concurrency, and the official HTML extension into an auditable collector for APIs and static documents.
Aug 27, 2026 · 9 min read

Data collection is a natural HHY workload.

A collector combines seeds, HTTP, retries, timeouts, concurrency, parsing, normalization, deduplication, failure records, and persistence. It brings HHY's Flow, Stream, Effect, and resource-boundary ideas into one real task.

I did not want to begin with “Scrapy for HHY.”

Scrapy carries mature URL scheduling, middleware, downloading, item pipelines, caching, robots.txt, and an extension ecosystem. Playwright adds browser processes, JavaScript, page lifecycle, and interactive state. Promising all of that in v0.1 would expand the language runtime, extension protocol, and framework API simultaneously, making it difficult to know which layer was actually stable.

I chose a narrower definition:

HHY Collector Framework is a Flow-first, auditable, resource-bounded data collector for APIs and static documents.

HHY Collector Framework implementation architecture

The first boundary: collector, not browser

The current version targets JSON APIs, CSV/JSON datasets, and static HTML. It does not execute JavaScript, handle login challenges, bypass robots.txt, authentication, or anti-bot controls, and it does not promise arbitrary response streams.

That is an architectural boundary rather than a deferred-feature disclaimer. HHY currently buffers an HTTP response body under max_http_body; parallel(n) is bounded and ordered by default. Those behaviors give documentation pages and ordinary APIs clear resource ceilings. They are not the right model for video, archives, enormous NDJSON, or browser-rendered applications.

A later Browser Worker can remain external. HHY can own plans, resources, data, and failures while a browser owns page execution. The runtime does not need to become a browser.

Do not disguise a DOM parser as Regex

HTTP was not the missing capability. Structured HTML was.

split, replace, and PCRE2 are useful for regular text, but they should not become a general HTML extraction layer. Real pages need error-tolerant parsing, nesting, entities, attributes, and selector semantics.

I implemented the official html process extension using Lexbor's HTML5 parser and CSS selectors. A schema looks like this:

{
  "root_selector": "section.card",
  "max_results": 10,
  "schema": {
    "title": {
      "selector": "h2",
      "value": "text",
      "required": true
    },
    "anchor": {
      "selector": "h2",
      "value": "attr",
      "name": "id"
    }
  }
}

The engine sends HTML, a root selector, the schema, and a result limit in one call:

html.extract(
    fetched.value,
    root_selector,
    schema,
    { max_results: max_results }
)

The extension returns an ordinary List<Map> that can rejoin an HHY Stream.

This API is deliberate. Process Extension Protocol values are Null, Bool, numbers, String, List, and Map. It cannot carry an opaque DOM handle or Stream. A browser-like parse → node handle → select → text interface would first require new protocol and lifetime semantics.

For v0.1, “HTML + Selector + Schema → List<Map>” keeps the DOM inside the extension process. It gives up arbitrary traversal in exchange for simple ownership, explicit serialization, and an immediately testable application API. Real projects can tell us whether a Node logical value eventually belongs in Core.

The engine has one main path

crawler.hhy owns arguments, configuration, statistics, and three atomic outputs. lib/engine.hhy owns execution:

export fn crawl(config) {
    let user_agent = config.user_agent
    let root_selector = config.root_selector
    let schema = config.schema
    let max_results = config.max_results

    return config.seeds
        |> stream
        |> distinct
        |> parallel(config.parallelism) { url ->
        crawl_seed(url, user_agent, root_selector, schema, max_results)
    }
        |> collect
}

Seeds are deduplicated before entering configured bounded concurrency. HTTP uses an identifiable User-Agent, a ten-second timeout, two retries, and 500 ms backoff. There are no unlimited workers or hidden global pools.

Each seed has two attempt boundaries: fetch and HTML extraction.

Execution flow from one seed to auditable output

let fetched = attempt { fetch_page(url, user_agent) }
if fetched.ok != true {
    return { ok: false, url: url, records: [], error: fetched.error.message }
}

let parsed = attempt {
    html.extract(fetched.value, root_selector, schema, { max_results: max_results })
}
if parsed.ok != true {
    return { ok: false, url: url, records: [], error: parsed.error.message }
}

Network and schema failures become normal results without erasing peer seeds. Every successful record receives source_url; collected data without provenance is difficult to audit, update, or correct.

Three outputs carry three responsibilities

The program writes:

  • records.json for extracted records;
  • report.json for requested, successful, failed, and record counts;
  • failures.json for failed URLs and structured errors.

All three use atomic writes. Successful records remain useful when some pages fail, and failure details do not disappear into stderr. At the same time, report.ok becomes false and the process exits 1, so automation cannot mistake partial output for complete success.

This is better suited to collection than stopping at the first failure, and more honest than always returning zero.

Configuration is the first Spider language

The default job fetches https://hhylang.dev/zh/learn/cli-reference and extracts main article h2 text plus IDs:

{
  "project": "HHY Documentation Crawler",
  "seeds": ["https://hhylang.dev/zh/learn/cli-reference"],
  "parallelism": 2,
  "user_agent": "HHY-Collector/1.0 (+https://hhylang.dev)",
  "root_selector": "main article h2",
  "max_results": 100,
  "schema": {
    "title": { "selector": "", "value": "text" },
    "anchor": { "selector": "", "value": "attr", "name": "id" }
  }
}

An empty selector reads the record root. Fields support text, html, and attr, plus all. max_results crosses the configuration and extension boundary, preventing a mistaken selector from returning thousands of nodes to the runtime.

The Spider is currently pure data. I deliberately avoided function references in JSON because it cannot reliably express executable closures. Pagination, follow requests, and normalize hooks should eventually come from .hhy Spider modules rather than code strings inside configuration.

Extension installation should stay project-local

init.sh installs the official html extension into the project's .hhy-extensions. It is idempotent and leaves the user's global extension home untouched.

make
./practical-projects/my-crawler/init.sh
./practical-projects/my-crawler/run.sh

Execution explicitly sets HHY_EXTENSION_HOME, allowing the project to carry its expected extension version rather than depending on something globally installed on one developer's machine.

Regression tests should not depend on the public internet

The live task proves that HHY can collect a real documentation site. The self-test proves the code contract.

self-test.sh starts a local fixture, creates configuration dynamically, selects two section.card records, and asserts titles First item and Second item, anchors one and two, local provenance URLs, two report records, and an empty failure list.

./practical-projects/my-crawler/self-test.sh

A public redesign should not turn the framework regression suite red. Conversely, a passing fixture does not prove that a live selector still matches. Smoke tests and deterministic tests provide different evidence.

It is not yet the complete framework I imagined

The implementation proves the minimum loop—Seed → Fetch → Extract → Provenance → Output—but does not yet include:

  • pagination and follow requests;
  • URL canonicalization and relative resolution;
  • per-domain admission and robots.txt;
  • request fingerprints and a visited set;
  • incremental checkpoints and failure replay;
  • conditional requests, cache, and content hashes;
  • JSON, CSV, and database storage adapters;
  • unordered completion such as parallel(..., { ordered: false }).

Most of these can be written in HHY without immediately changing the runtime. Real Spiders should drive priority. I would add pagination, request identity, checkpoints, and domain admission first, then measure whether ordered parallelism is a genuine throughput bottleneck.

What the implementation reinforced

A framework's value does not come from having the most concepts. It comes from making failure, resources, and effects visible.

HHY Collector Framework currently has one engine, one declarative schema, and one HTML extension. Yet every request has a timeout, every batch has bounded concurrency, every extraction has a result ceiling, every record has provenance, every run has a failure audit, and every file is written atomically.

Those boundaries matter more than starting with dozens of middleware names.

I did not begin by cloning Scrapy because HHY does not need to imitate another ecosystem to justify itself. It should first prove that a collection flow can remain direct while staying dependable around networks, parsing, and filesystem effects.

References

18 / 41

Optimizing HHY's AST Interpreter Without Switching to Bytecode

How HHY Language v1.1.1 reduced function-call overhead with a resolver, static slots, lightweight call frames, identifier caches, and escape-safe reuse—without changing language semantics.
Aug 27, 2026 · 11 min read

HHY Language v1.1.1 is still an AST interpreter.

After lexing, parsing, and checking, the runtime walks the AST directly. There is no bytecode lowering stage and no JIT. For a systems scripting language, this path has useful properties: source locations remain close to execution, errors are easier to explain, and a semantic change does not require a second instruction system to evolve in lockstep.

Direct execution, however, should not mean rediscovering the same facts on every call. Under recursion, closures, and call-heavy workloads, the expensive questions become repetitive: Which lexical scope owns this name? Where should an argument live? Does every call require a general-purpose environment object? Can the frame be reused safely after return?

The goal of this work was intentionally narrow:

Keep the AST interpreter, but remove work that can be resolved ahead of time, addressed directly, or reused safely.

The optimized execution path in HHY's AST interpreter

Profile first, rewrite later

Interpreter work can easily begin with an attractive solution: define opcodes, write a compiler, implement a VM, and expect speed to follow. Bytecode is not a free abstraction. It adds an intermediate representation, control-flow rules, debug mapping, stack traces, closure capture, and another compatibility surface.

I prefer to start with a smaller question: where is the current cost?

HHY v1.1.1 includes a profiler:

hhy profile examples/09-profile-algorithms.hhy -- fibonacci 20
hhy profile --cpu examples/09-profile-algorithms.hhy fibonacci 20
hhy profile --heap --format json --output profile.json \
  examples/09-profile-algorithms.hhy fibonacci 20

profile executes the real script. CPU reports include samples and call counts. Heap reports include managed allocations, object counts, peak usage, and live memory after GC. Reports go to stderr by default, leaving script output and exit status intact.

The baseline also quantified the problem. Naive Fibonacci(30) enters an HHY function 2,692,537 times. An early runtime path produced 32,310,598 allocations and about 1.98 GB of cumulative allocation traffic. That was not 1.98 GB live at once; it was millions of calls creating and quickly discarding short-lived objects.

The data separated call overhead into two parts:

  1. name resolution and environment lookup repeated inside hot calls;
  2. general-purpose environments allocated for calls that needed much less state.

Neither problem required bytecode first. Both required the runtime to stop forgetting facts the language front end already knew.

With the optimized path, the same Fibonacci(30) benchmark—three warmups and twenty fixed, randomly interleaved runs—moved from a 681.27 ms median to 200.79 ms, a cumulative 3.39× improvement. More importantly, the algorithm and all 2,692,537 function calls remained unchanged:

MetricEarlier pathSlot FrameChange
HHY median681.27 ms200.79 msabout 3.39×
Profile allocations4,038,9471,346,440-66.7%
Cumulative allocation328.7 MiB61.6 MiB-81.3%
Peak heap2.1 MiB2.1 MiBessentially unchanged

Fibonacci is not HHY's target workload, but it magnifies function-call cost. The result shows that the general runtime path does less work; it does not imply the same end-to-end speedup for file, HTTP, or process-heavy flows.

Resolve pass: move stable decisions before execution

In the original path, an Identifier node carried a name. When the runtime reached total, it searched from the current environment outward until it found a binding, then continued to globals and builtins if needed.

That is semantically correct and mechanically wasteful. The lexical owner of one AST node usually does not change on its ten-thousandth execution.

The v1.1.1 resolver walks the AST before function execution and classifies names where possible:

  • parameters receive slots in the current function;
  • deterministic locals receive slots;
  • cross-function closure captures, globals, and builtins retain the Env path;
  • scopes containing dynamic import do not receive a forced static layout;
  • identifiers that cannot use a direct slot receive reusable lookup metadata.

This is not merely a string cache. It turns stable lexical relationships into metadata the runtime can address directly.

Identifier("total")
  before: lookup(env, "total")
  after:  slots[3]

The parser still owns structure. The checker still owns semantic constraints. The resolver translates proven scope facts into a faster runtime representation.

Slots: locals do not need repeated name lookup

Once parameters and deterministic locals live in slots[], reads change from a name-based environment walk to an indexed array access.

Local / Param       → slots[index]
Closure / Global    → Env lookup
Builtin             → Env lookup

I deliberately did not force every name into a slot. Captured variables outlive individual calls, globals belong to a shared namespace, and builtins participate in runtime registration. Keeping the compatible Env path for those cases preserves a clear model and avoids complex invalidation rules.

The principle is simple: the fast path serves the common, provable case; the slow path preserves complete semantics. They are two responsibilities, not an unfinished compromise.

Slot fast path and compatible Env lookup in HHY

Lightweight call frames: a call is not a full environment

Slots answer where values live. The next question is what a call must create.

A function call needs a relatively small core of state: its function, parameter and local slots, a parent or captured environment when necessary, plus runtime state for return and error propagation. Building a general-purpose map-backed environment on every call increases lookup cost and managed allocation.

The hot path now uses a lightweight call frame. The implementation does not introduce a second object model beside Env. Instead, Env carries contiguous Binding capacity, an escaped flag, and a free-list link:

CallFrame
├── function
├── slots[]
├── parent / captured env
└── runtime state

Each call acquires capacity from the resolver's frame_slot_count. Arguments are written in order and deterministic locals then occupy stable slots. A function-body block uses the call frame directly instead of allocating another parent-only block environment. Lookup walks the compatible Env chain only for closures, globals, or dynamic paths.

This does not turn a dynamic language into a static one. HHY values remain dynamic. What becomes stable is the binding location: a value's type may be determined at runtime while a proven local name no longer needs its address rediscovered on every read.

Identifier caches: the slow path can still remember

Not every identifier can use a slot, but that does not mean every lookup must begin from zero.

For Env-based identifiers, the AST node caches cached_env_depth and cached_binding_slot. A later execution first follows that depth and slot, then compares the binding name's length and bytes again. Only a validated name is a hit. If the environment shape no longer matches, the runtime invalidates the cache, performs a full lookup, and rebinds it.

The cache must belong to a semantically stable object and have explicit invalidation. A process-wide map from name to address would incorrectly share results across recursion, modules, and closures. HHY's cache follows the AST node and its scope relationship, not the string alone.

The most dangerous optimization is not one that fails to improve performance. It is one that is faster on most inputs and occasionally reads the wrong binding. A cache hit must never bypass lexical semantics.

Frame pools: decide escape before reuse

Lightweight frames still need initialization. In a call-heavy function, discarding each frame and waiting for GC continues to create allocation pressure.

A pool is the obvious next step, but a frame cannot be reused merely because the function returned. A closure or stream may retain the current environment. The stack call is over; the object lifetime is not. Resetting that frame would make an older closure observe values from a newer call.

HHY therefore gives the frame pool a hard boundary:

non-escaping frame  → reset → pool → reuse
captured by closure → mark escaped → no reuse → GC managed
captured by stream  → mark escaped → no reuse → GC managed

That is why I think of the design as escape-safe reuse rather than a generic object pool. Pooling is the mechanism. Escape classification provides correctness.

What an optimization must not change

The acceptance criterion is not only a faster benchmark. Language promises must remain intact:

  • errors still point to original source locations;
  • closure capture and lifetime remain unchanged;
  • shadowing across locals, modules, globals, and builtins remains unchanged;
  • stream laziness, single consumption, and cancellation remain unchanged;
  • hhy check, hhy ast, --dry-run, and real execution share one meaning;
  • resource limits, exit codes, and unwind paths cannot be bypassed by a fast path.

That calls for regression tests around recursion, deep scopes, shadowing, escaping closures, stream capture, stack traces, and objects that remain alive after GC—not just a timing fixture. The new frame-slots-escape.hhy creates two independent counter closures and mutates their captured slots after the factories return. It also returns a lazy stream that captures a local and is only collected after its function has returned. Its output—11, 12, 101, and [8, 9, 10]—shows that frames were not incorrectly reused and delayed stream evaluation did not observe cleared state.

The full release suite, ASan/UBSan suite, and Multi-API Data Collector self-test also have to pass. An optimizer should be removable without changing program results.

Why HHY does not use a bytecode VM yet

A bytecode VM remains a possible direction, but data should trigger it—not the aesthetic of a roadmap.

After the resolver, slots, lightweight frames, and safe reuse are in place, profiling may still show AST dispatch as the dominant remaining cost. If that cost also matters in real flows, modules, and system-call-heavy workloads, bytecode will have a clear case. The resolver work will remain useful because it already organizes names, slots, and closure boundaries into lowering-friendly facts.

If a script spends most of its time in files, HTTP, processes, or stream operators, cutting AST dispatch in half may barely affect end-to-end latency. A systems scripting language cannot optimize only for Fibonacci.

My current sequence is:

Profile
  → eliminate repeated semantic work
  → direct-address stable bindings
  → reduce call-frame allocation
  → reuse only non-escaping state
  → profile again
  → consider bytecode when AST dispatch becomes the bottleneck

The lesson I am keeping

This work reinforced a useful idea: performance problems are often not caused by too much abstraction. They happen because the system fails to consume facts it has already proved.

If the front end knows the scope of a local, the runtime should not guess again. If a frame does not escape, it does not need a full GC lifetime. If an identifier's environment location is stable, even the compatible path can avoid starting from the root.

The best optimizations do not skip semantics. They turn semantic proof into a shorter execution path.

HHY v1.1.1 still executes the AST directly. That is not a statement that bytecode is unimportant. It means the more valuable step today is to make the AST interpreter repeat less work while keeping source diagnostics, closures, streams, and resource boundaries dependable.

References

19 / 41

Researching Hong Kong Film Companies with HHY and MediaWiki

A reproducible HHY research flow for candidate search, concurrent page details, semantic filtering, provenance, and deterministic regression tests.
Aug 27, 2026 · 6 min read

“Collect Hong Kong film companies” sounds like a small crawler exercise. In practice, HTTP quickly gives way to research questions: Is a search result actually a company? What should tests do when external data changes? Should one failed page discard the batch? How does output retain provenance?

This project uses the official MediaWiki API. It searches for pages related to “香港電影公司”, concurrently fetches page IDs, canonical URLs, timestamps, and introductions, applies an explainable company filter, then atomically writes CSV and JSON.

The actual Hong Kong film companies project layout

Search and detail are separate stages

Search discovers candidates; it is not final data. The flow retrieves query.search, deduplicates by pageid, and requests details through bounded parallel work.

return search_results
    |> stream
    |> group_by { result -> result.pageid }
    |> map { group -> group.values[0] }
    |> parallel(parallelism) { result ->
        let detail = attempt { /* MediaWiki detail request */ }
        // return success or structured failure
    }
    |> collect

Requests have a ten-second timeout, two retries, and 250 ms backoff. attempt isolates each page, so a timeout on page seven does not erase six completed results.

Introductions are capped at 600 characters. This is not merely bandwidth thrift; it bounds the value carried back from each worker. Unbounded external responses can turn concurrency throughput into memory pressure.

The filter admits that it is a heuristic

The final rule requires the introduction to mention Hong Kong, film, and a company, then sorts by title. It is explainable and testable, but it is not a corporate registry.

On August 26, 2026, the real run retained seven of ten candidates, including individual companies and the Hong Kong film-company list. I would rather state that boundary explicitly than present search output as an authoritative directory.

Actual HHY network run against Wikipedia

CSV keeps the canonical Wikipedia URL, page ID, timestamp, and introduction so a reader can verify the source. Republishing introductions also requires attention to the licenses and attribution identified by their pages.

Dynamic research and regression testing are different evidence

Wikipedia changes and networks fluctuate. A test that insists there are still seven results tomorrow mostly tests the internet, not the program.

self-test.sh therefore starts a local MediaWiki fixture. Five fixed candidates pass through the same deduplication, concurrent fetch, and semantic filter and must produce three companies. Assertions cover CSV fields, ordering, URLs, and JSON statistics.

sh practical-projects/hong-kong-film-companies/self-test.sh

The live run answers “what can I find now?” The fixture answers “does the program still honor its contract?” Both matter, but they should not be confused.

One compatibility detail

The Chinese search term is percent-encoded in configuration because HHY 1.1.1 still has a limitation around non-ASCII query Map values. ASCII detail parameters continue to use the structured query Map. I prefer documenting that boundary over hiding it behind an implicit workaround.

This is what practical projects add to a language roadmap: real APIs, real characters, and real failure modes turn a feature list into engineering work.

References

20 / 41

Building HHY Language: From Flow Philosophy to a C Runtime

A ground-up account of HHY Language v1.0: why flow is the central abstraction, how the lexer, parser, AST, checker, and runtime fit together, and what turns a language implementation into a dependable system.
Aug 25, 2026 · 17 min read

Over the past few months, I built a systems scripting language from scratch in C: HHY Language.

It is not a new spelling for a handful of shell commands, and it is not a natural-language wrapper around an executor. HHY has its own tokens, grammar, AST, scopes, semantic checker, runtime value model, lazy streams, structured errors, and resource limits. From the moment source enters the lexer to the point where a file is opened, a process starts, or an HTTP request is sent, every step has defined behavior.

HHY Language Logo

The positioning is deliberately simple:

Pipe Everything. Bring files, processes, networks, and structured data into one Flow.

path("./logs")
    |> files("**/*.log")
    |> flat_map { file -> read_lines(file.path) }
    |> where { line -> contains(line, "ERROR") }
    |> take(100)
    |> save_lines(path("errors.txt"))

This article is not only a syntax tour. It is a retrospective on what actually matters when a language moves from an idea to a frozen v1.0 contract.

Why build another language?

Systems automation does not lack tools. Shell is excellent at connecting processes. Python is excellent for complete programs. jq, awk, and curl each solve a focused class of problems. Friction appears when one task crosses directory traversal, text streams, JSON, HTTP, concurrency, and error recovery: the programmer keeps switching mental models.

I wanted to test whether those objects could be expressed as sources, transformations, and actions—so that a program mostly describes how data moves rather than manually coordinating every step.

source |> transform |> filter |> action

That makes |> more than visual punctuation. It is a composition rule shared by the language, standard library, and runtime:

x |> f           => f(x)
x |> f(a, b)     => f(x, a, b)
x |> obj.f(a)    => obj.f(x, a)

Once that rule is stable, files, processes, HTTP requests, and ordinary collections can share one expression model. A language philosophy becomes meaningful only when it becomes executable semantics.

Freeze semantics before expanding features

The easiest trap in language design is to keep adding keywords and library functions. The harder questions look smaller: When does a newline terminate a statement? Is / division or the start of a regular expression? How do maps and blocks share {}? Does a pipe flatten a nested stream? Is parallel output ordered? Who closes upstream resources after an error?

I made docs/HHY_V1.md the single source of truth. Before expanding the implementation, v1.0 froze a set of decisions: dynamic typing, immutable bindings by default, explicit closures, one call syntax, lazy single-consumption streams, fail-fast errors, request construction separated from send, and bounded ordered parallelism.

The order matters. The specification is not documentation written after the code; it is a constraint applied before changing the code. When an experiment conflicts with the specification, the language decision comes first, then the spec and tests, then the implementation.

From source text to effects

HHY currently executes the AST directly, but it still has a complete language front end:

UTF-8 Source
  → Lexer / Token
  → Parser / AST
  → Checker / Scope & Contract
  → Runtime / Value & Environment
  → Stream / System Effect

Each layer answers a different question. The lexer does not resolve names. The parser does not open files. The checker does not send HTTP. The runtime does not rediscover operator precedence. Clear boundaries make diagnostics more precise and evolution safer.

The lexer: turning characters into located facts

The lexer turns UTF-8 source into tokens. Every HHY token retains its kind, raw source slice, line, and column. Location is not a debugging accessory; it is the evidence used by parser, checker, and runtime diagnostics.

Lexing still requires context. /ERROR/i and total / count both contain /; HHY decides between regex and division by asking whether the previous token can end an expression. Native values such as 10mib, 500ms, and 80% become Bytes, Duration, and Percent tokens instead of strings interpreted later by library calls.

The lexer validates UTF-8, rejects unsupported string escapes, and preserves newlines as tokens. Because a pipeline may continue across lines, blindly discarding whitespace would lose grammar-relevant information.

The practical rule is: tokens should retain enough source truth without taking over semantic work. Once the lexer starts understanding scopes or function contracts, the architecture has already blurred.

The parser: encoding precedence and context

HHY uses a recursive-descent parser. Expression rules build progressively through postfix calls and member access, unary operators, multiplication, addition, comparison, logic, null coalescing, pipes, and assignment.

The advantage is not merely implementation simplicity. Each grammar rule remains visible in code. Maps and blocks both use braces, but appear in different grammatical positions; closures are recognized when a call stage provides the relevant context. It is cleaner to decide there than to invent multiple brace tokens in the lexer.

Error recovery matters as much as successful parsing. Stopping after one missing expression forces users into a slow one-error-at-a-time loop. Continuing without synchronization produces cascades of false errors. HHY synchronizes around declarations, control flow, newlines, and semicolons so one hhy check can report multiple real problems without consuming the rest of the file as part of a broken expression.

Why the AST matters

The abstract syntax tree is the central representation between surface syntax and execution. It discards punctuation that no longer matters while preserving program structure.

processes
    |> where { process -> process.memory > 1gb }
    |> take(10)
    |> print

Conceptually becomes:

Pipe
├── Pipe
│   ├── Pipe
│   │   ├── Identifier "processes"
│   │   └── Call "where"
│   │       └── Closure
│   │           └── Binary ">"
│   └── Call "take"
│       └── Literal "10"
└── Identifier "print"

An HHY AST node contains a kind, its source token, and child nodes. Node kinds cover declarations, control flow, calls, member access, closures, pipes, lists, maps, and literals. The source token remains attached through runtime execution, which allows runtime errors to point back to the expression the user wrote.

The AST creates four important boundaries:

  1. The parser decides what structure the source represents.
  2. The checker can inspect the program without executing effects.
  3. The runtime executes stable nodes instead of parsing text again.
  4. hhy ast, the formatter, snapshot tests, and future optimization passes can share one representation.

A useful AST should not mechanically preserve every punctuation mark, and it should not lower too early into instructions tied to one runtime. It must be abstract enough for semantics while retaining source links for diagnostics and tooling.

The checker: useful certainty in a dynamic language

HHY is dynamically typed, but dynamic does not mean every error should wait until runtime. The checker catches undefined names, duplicate bindings, assignment to immutable bindings, arity mismatches, return outside a function, break outside a loop, circular module imports, and missing exports.

It also protects concurrency boundaries. A parallel closure cannot capture mutable bindings or a single-consumption stream because those values are unsafe to share across workers. Rejecting that before workers start is more dependable than discovering it as a race.

Standard functions register more than a C function pointer. Each HHY callable has a contract describing arity, effect category, laziness, cancellation support, sendability, input/output constraints, and threading. The checker, runtime, dry-run planner, and future extension protocol all consume the same contract.

The principle is straightforward: if a problem can be proven before an effect begins, do not defer it to runtime.

The runtime: where the language becomes responsible for consequences

The runtime owns values, environments, calls, error propagation, and system resources. HHY values include Null, Bool, Int, Float, String, List, and Map, but also Regex, Bytes, Duration, Percent, Path, File, Process, HttpRequest, DateTime, Function, and Stream.

When a Pipe node executes, the runtime evaluates the left side and injects the resulting value into the callable on the right. One rule implements the language semantics instead of adding grammar for every library operation.

The interesting part of a systems language is not computing 1 + 1; it is handling consequences. Are files closed? Is a timed-out process reaped? Is TLS verified? Does cancellation propagate through a stream? Does a failed write leave a damaged output file?

HHY uses a constrained conservative tracing GC for language heap values, while files, processes, and network handles close explicitly. GC answers when language objects are unreachable. It cannot answer when an operating-system resource must be released. Conflating those lifetimes may survive short scripts and fail under long flows or error paths.

Streams: laziness as an execution protocol

HHY streams are pull-based, lazy, and single-consumption. where, map, and take do not materialize the whole input. Each downstream request pulls one value through the chain.

sink.next()
  → take.next()
  → where.next()
  → files.next()
  → one value returns downstream

Large files and directories stay bounded in memory. take(10) can stop upstream early. Errors and cancellation can close the chain.

Laziness still needs explicit barriers. sort_by, group_by, and some reductions must buffer input, so the runtime enforces memory and record limits. parallel is bounded too: maximum concurrency, finite buffering, ordered output, fail-fast behavior, and cleanup on cancellation.

I came away with a stronger belief that the difficult part of a concurrency API is not how work starts, but how it stops. Early termination, error, Ctrl-C, and timeout paths need one unwind model; fast happy-path execution is not enough.

Explicit effects and safe defaults

HHY separates describing an action from performing it where practical. http.get constructs an HttpRequest; send performs network I/O. run receives an argument array and does not invoke a shell; shell semantics require explicit shell. File saves prefer atomic replacement.

The runtime imposes limits on memory, open files, processes, parallelism, HTTP bodies, regex steps, recursion, and total runtime. hhy run --dry-run produces a redacted execution plan without external effects. That feature is credible because it uses the AST and callable contracts, not string matching.

Testing a language means testing failure

The dangerous behavior is often outside valid examples. HHY therefore tests:

  • exact token and AST snapshots;
  • parser recovery and invalid escapes;
  • checker failures for scopes, arity, modules, and parallel captures;
  • integer overflow, resource unwind, cancellation, timeout, and structured runtime errors;
  • acceptance flows for files, processes, HTTP, JSON, CSV, file watching, and parallel work;
  • ASan, UBSan, and coverage-guided fuzzing;
  • native CI on macOS arm64, Linux arm64, and Linux x86_64;
  • parsing and checking the HHY code blocks in the README and specification;
  • release archives, notices, dependencies, and SHA-256 checksums.

Executable documentation is especially valuable. If examples never enter CI, language evolution will eventually turn them into plausible-looking programs that no longer run.

What I deliberately left out

HHY v1.0 has no JIT, no static type system, and no public native ABI. Extension boundaries exist, but the first stable runtime does not load unknown third-party extensions.

These are scope decisions, not forgotten features. A language earns trust by fully implementing a smaller set of promises. Direct AST interpretation let me establish semantics, errors, resource behavior, and portability first. Dynamic typing matches the feedback loop of systems scripting. Delaying a native ABI avoids freezing internal structures before they are ready to carry long-term compatibility.

The lasting lesson

Building HHY changed how I think about abstraction. An abstraction is not a better name around code; it is a constraint that remains consistent across layers.

|> becomes Flow-first only when the parser associates it correctly, the AST represents it precisely, the checker validates the callable, the runtime propagates cancellation, and the library obeys the lazy protocol. Otherwise, it is only an operator.

The AST matters, but it is not the destination. Parsing matters, but successful parsing does not imply dependable execution. The runtime matters, but a runtime without a specification and evidence is only the accidental behavior of the current build.

My working definition is now:

language = syntax + semantics + execution model + effect boundaries
         + diagnostics + tooling + compatibility promises

HHY v1.0 is still young, but it can already apply one Flow model to files, processes, networks, and structured data—and explain what it will do, where it failed, how it stops, and how it releases resources. That is much closer to the problem I wanted to solve than simply inventing another syntax.

Project

21 / 41

Why I Put OCR Before the LLM: Image Understanding Is More Than Uploading a File

A production account of how Shixiseng OCR Service validates, preprocesses, rasterizes, recognizes, and structures images and scanned PDFs before sending reliable context to an LLM.
Aug 24, 2026 · 17 min read

I built Shixiseng OCR Service not because multimodal models cannot read images, but because reading one image successfully and processing thousands of business documents predictably, economically, and traceably are very different problems.

Recruiting workflows receive résumé screenshots, certificate photos, chat records, scanned PDFs, and skewed tables. The shortest demo is to send the original image to a multimodal model and ask for names, schools, employers, dates, and projects. It often looks impressive. In production, however, the same image may produce different structures across calls; orientation, clarity, and document length affect results; long PDFs are expensive; and when a value is wrong, it is difficult to tell whether the failure happened during visual reading, context construction, or reasoning.

My solution is an independent OCR layer before the LLM. OCR does not replace the model. It separates reading the characters from understanding what they mean: OCR produces text, pages, coordinates, and confidence scores; the LLM performs normalization, extraction, and business reasoning.

The OCR processing layer between source documents and the LLM

Separate perception from understanding

Sending the original image directly to a model makes one request responsible for decoding, text recognition, layout understanding, field extraction, and reasoning. When it fails, the only evidence is usually an incorrect-looking JSON object.

I split the pipeline into explicit stages:

Image / scanned PDF
  → file authenticity and resource-limit checks
  → page rasterization and conditional preprocessing
  → text detection and recognition
  → text, pages, boxes, and confidence
  → LLM extraction, normalization, and reasoning
  → business validation and human review

The intermediate representation matters. If a model turns 2023.08 into 2028.03, I can inspect the page, OCR line, box, and confidence score. If OCR was correct, the prompt or field rules need work. If OCR was already wrong, the image pipeline, engine, or fallback policy is responsible. The system can finally answer where the error occurred.

OCR also reduces irrelevant multimodal input. Blank pages, repeated pages, decoration, and low-value areas can be filtered. Text can be grouped by page or block instead of sending dozens of high-resolution images. At scale, that changes latency, token cost, and concurrency capacity.

The current production baseline

Shixiseng OCR Service accepts JPG, PNG, WEBP, BMP, TIFF, and PDF. It provides synchronous and asynchronous APIs that share the same recognition use case.

The synchronous endpoint handles a small image or short document in the request. The asynchronous endpoint persists task metadata and a temporary file, delegates recognition to a Celery worker, and returns a job_id. There are not two OCR implementations—only two scheduling modes.

The response is more than a plain string:

{
  "engine": "rapidocr-onnxruntime",
  "page_count": 2,
  "text": "full text...",
  "pages": [
    {
      "page_number": 1,
      "width": 1440,
      "height": 2036,
      "lines": [
        {
          "text": "2023.08 - 2025.06",
          "confidence": 0.986421,
          "box": [[112, 284], [486, 284], [486, 326], [112, 326]]
        }
      ]
    }
  ],
  "duration_ms": 1260
}

Full text is convenient for LLM context, page text preserves document boundaries, boxes support evidence lookup and reading-order reconstruction, and confidence can trigger a retry or review. This structure has more lasting value than a single OCR string.

What the service is actually built on

This is not a thin PaddleOCR wrapper. Python 3.11 is the production baseline, and the HTTP layer, document processing, OCR engines, and task execution are separated. The current release pins these core versions:

LayerPinned componentResponsibility
CPU OCRRapidOCR 3.4.2 + ONNX Runtime 1.22.1Default production baseline for detection and recognition
GPU OCRPaddleOCR 3.2.0 + a CUDA-compatible PaddlePaddle GPU wheelHigh-throughput recognition on GPU nodes
DocumentsPyMuPDF 1.26.4Decoding, dimension checks, PDF paging, and RGB rasterization
HTTP APIFastAPI 0.141.1 + Uvicorn 0.52.0Uploads, authentication, schemas, synchronous API, and job queries
ProcessesGunicorn 23.0.0 + uvicorn-worker 0.4.0Workers, timeouts, graceful shutdown, and request recycling
Async workCelery 5.6.3 + Redis client 6.4.0Queues, state, retries, time limits, idempotency, and rate limiting
ConfigurationPydantic Settings 2.10.1 + Structlog 25.4.0Typed startup validation and structured JSON logs

“Pinned” does not mean permanently best. It means this exact combination is represented in the lock file, automated tests, and release process. Production stability comes from a validated combination, not from installing whatever happens to be latest during deployment.

Why RapidOCR is the CPU default

RapidOCR provides a practical OCR layer over multiple runtimes; this service explicitly uses ONNX Runtime for CPU inference. It has a smaller deployment surface than a full PaddlePaddle installation, requires no CUDA, and is suitable for ordinary Rocky Linux or AlmaLinux x86_64 servers.

The adapter converts RapidOCR version-specific output into the service-owned PageResult model. API consumers never receive third-party SDK objects, so the engine or model can change without changing the external JSON contract.

Where PaddleOCR fits

PaddleOCR is the optional GPU implementation, not a hidden dependency of the CPU build. The project pins paddleocr==3.2.0, but deliberately does not put one universal PaddlePaddle GPU wheel in the generic runtime lock. The correct wheel depends on the NVIDIA driver, CUDA version, operating system, and Python ABI.

On a GPU build node, I first verify the environment with nvidia-smi, install the PaddlePaddle GPU package from the official compatibility matrix, install PaddleOCR, and then run acceptance tests with real documents. Startup calls paddle.is_compiled_with_cuda(). Strict gpu mode fails readiness when CUDA is unavailable; auto mode may emit a warning and fall back to RapidOCR CPU.

What “stable” means here

I do not treat a project's latest tag as a production definition. A stable OCR baseline needs all of the following:

  1. A known Python, Linux, glibc, CPU instruction-set, or CUDA combination.
  2. Exact package versions and hashes for reproducible builds.
  3. Pre-downloaded, versioned model weights; startup must not depend on the public internet.
  4. Regression results on real Chinese résumés, screenshots, certificates, and scanned PDFs.
  5. Verified synchronous, asynchronous, timeout, retry, restart, and CPU-fallback behavior.

PaddleOCR 3.2.0 is only a package number. PaddleOCR 3.2.0 + named model weights + compatible PaddlePaddle/CUDA + target-host acceptance results is a deployable baseline.

Input handling comes before model tuning

The service does not trust filename extensions or a client-provided Content-Type. It detects the format from the file signature and enforces upload size, total image pixels, PDF page count, per-page render size, and decodability. Encrypted PDFs, damaged files, and oversized images are rejected before entering an OCR engine.

This is both an accuracy and a reliability boundary. A decompression-heavy image can exhaust memory, an unbounded PDF can monopolize a worker, and a disguised file should not be passed blindly to a parser.

PDF pages are rendered by PyMuPDF to RGB PNG at a default 180 DPI, without an unnecessary alpha channel. Each page image is released immediately after recognition. DPI is not “the higher, the better”: doubling width and height roughly quadruples the pixel workload. The correct baseline comes from measuring accuracy and throughput on the actual corpus.

Preprocess according to the failure mode

A generic recipe—grayscale, binarize, sharpen, upscale—often damages faint text, stamps, or table rules while adding latency. I prefer conditional processing:

Observed problemTreatmentRisk to watch
90°/180° rotationorientation classification and rotationdo not rely only on EXIF
Perspective distortioncorner detection and unwarpingclipping page edges
Uneven light or gray backgroundlocal contrast or adaptive thresholdinglosing faint text or stamps
Small textbounded upscaling or higher PDF DPIsharp memory and latency growth
Slight skewbaseline-angle estimation and correctiontable lines biasing the angle
Compression noiselight denoisingdeleting character strokes
Multiple columnslayout segmentation before OCRincorrect reading order

The current service implements format detection, resource limits, PDF rasterization, and normalized RGB input. Orientation correction, unwarping, and layout recovery are not yet a standalone production stage. I plan to add them through a versioned DocumentPreprocessor, so every transformation can be disabled, tested, and compared.

CPU is the baseline; GPU is a capacity decision

GPU does not automatically mean better accuracy. Hardware primarily changes throughput and the practical model size. Accuracy still depends on the model, language, input quality, and domain data. A CPU fallback remains essential because the NVIDIA driver, CUDA runtime, PaddlePaddle wheel, and model must all be compatible.

Models are warmed during startup. Readiness passes only when the OCR engine, state Redis, and Celery broker are available. Model files live in a shared directory and are downloaded before release, so the first real user never becomes an accidental initialization test.

Synchronous and asynchronous paths are different capacity models

Small images are simplest synchronously, while a long PDF should not occupy a web worker. Synchronous recognition has a total timeout and tells the caller to use the job API when exceeded. Asynchronous work has soft and hard time limits and bounded exponential retries only for retryable failures.

Async submission supports Idempotency-Key, preventing duplicate jobs when a caller retries after a network timeout. Redis stores atomic task-state transitions; terminal tasks delete temporary files and expire results by TTL.

The OCR representation can also be cached by document hash, engine version, model version, and preprocessing version. Changing a prompt or LLM should not require reading the image again.

Publicly reachable does not mean unprotected

Business endpoints use Bearer tokens, and production refuses to start without sufficiently strong credentials. Current and previous tokens can coexist during rotation. Nginx handles TLS, request limits, and edge rate limiting; the application applies a second per-client sliding window.

The service runs under a non-login ocr-service account with hardened systemd settings. Logs contain request ID, client, format, size, pages, engine, duration, and error code—but not OCR text, tokens, or complete local paths. A request ID connects Nginx, API, worker, and job state without copying personal information into logs.

Deployment model

The production baseline is Rocky Linux 9 or AlmaLinux 9, Python 3.11, Redis 6+, Nginx, systemd, and Jenkins. Gunicorn with Uvicorn workers serves the API; separate Celery workers handle long tasks. CPU and GPU dependencies are built separately, and virtual environments are never copied from a developer machine.

Releases are immutable:

/opt/ocr-service/
├── releases/20260824-<git_sha>/
├── shared/.env
├── shared/models/
├── shared/tmp/
└── current -> releases/20260824-<git_sha>/

Jenkins runs Ruff, Mypy, pytest, coverage, dependency audit, and wheel build on a compatible Linux node. It produces a SHA-256-verified archive. The host creates a new venv, installs hash-locked dependencies, atomically moves the current symlink, and restarts the worker and API. A failed smoke test switches the symlink back.

After deployment I verify process liveness, readiness, one synchronous recognition, and one asynchronous job from queued to succeeded. active (running) alone does not prove that the model, Redis, and broker work.

A concrete OCR best-practice checklist

1. Build an evaluation set before tuning

Sample real traffic by failure type: native PDFs, scanned PDFs, phone photos, skew, shadows, low resolution, two-column résumés, tables, stamps, and mixed Chinese-English text. Keep tuning and regression sets separate.

CER is useful for OCR mechanics, but business metrics matter more: exact match for names and phone numbers, normalized-date accuracy, and recall of education and employment history. I track preprocessing deltas, page P50/P95, peak memory, low-confidence rate, human-review rate, final LLM field accuracy, and cost per document.

2. Extract native PDF text before applying OCR

If a PDF already has a trustworthy text layer, PyMuPDF extraction is normally more accurate and cheaper than rasterizing it. OCR should handle pages with no text layer, a broken hidden layer, or image-dominant content. The decision should consider text length, printable-character ratio, coordinate validity, and sampling—not merely whether a few characters were returned.

3. Make preprocessing optional, versioned, and replayable

Record the reason, input and output dimensions, and configuration version for every transform. Never overwrite the only original. PaddleOCR 3.x includes orientation classification and document unwarping, but enabling them must be justified by corpus-level measurements. Phone photos may benefit; clean scans may only become slower.

4. Do not confuse OCR output order with reading order

Sorting by Y then X can work for one column. Two-column résumés, tables, and sidebars need layout segmentation. LLM input should retain explicit page and block boundaries; otherwise every character can be correct while the reconstructed document is wrong.

5. Use confidence for routing, not as an absolute probability

Confidence values are not directly comparable across engines and models. Calibrate thresholds on domain data. High-confidence lines proceed; medium-confidence regions can be enlarged or sent to another engine; critical low-confidence fields can include an image crop for a multimodal model; unresolved conflicts go to human review.

6. Size concurrency from peak memory

OCR resource usage depends on pixels, models, inference threads, and simultaneous pages. Web workers, ONNX threads, and Celery concurrency must be budgeted together. The current Celery concurrency=1 is a conservative starting point, then load testing determines safe growth. GPU workers also need limits because each process may load another copy of the model into VRAM.

7. Diff every engine or model upgrade

Run old and new versions over the same regression documents. Compare text, field metrics, latency, and memory, then inspect regressions in numbers, dates, names, and mixed-language content. Canary the new version, write version metadata into results, and keep the old release and model available for rollback.

What I preserve when calling the LLM

The model input should not be only the OCR text field. I retain page and block boundaries, source page and confidence for critical fields, image crops for uncertain regions, and table cells or Markdown instead of flattened text.

The model can then return a field together with evidence and uncertainty:

{
  "field": "graduation_date",
  "value": "2025-06",
  "evidence": {
    "page": 2,
    "text": "2023.08 - 2025.06",
    "ocr_confidence": 0.986421
  },
  "needs_review": false
}

This does not make an LLM infallible. It makes failures visible, replayable, and measurable—which matters more in recruiting, contracts, finance, and records than a single impressive response.

Final judgment

Not every image needs OCR first. Direct multimodal input is often better for scene understanding, chart questions, visual relationships, or a few ad-hoc images. But when the task is text-centered and requires batch processing, evidence lookup, privacy controls, reproducibility, and predictable cost, an independent OCR layer remains valuable infrastructure.

Shixiseng OCR Service currently establishes the reliable baseline: safe image and PDF ingestion, page-level recognition, structured output, CPU/GPU engines, sync/async execution, authentication, rate limiting, idempotency, observability, and rollback. The next improvement is not blindly choosing a larger model. It is building a real failure corpus and adding conditional preprocessing, layout recovery, confidence routing, and field-level evaluation.

The goal is not a beautiful OCR benchmark score. It is to turn an image into cleaner, explainable, and trustworthy context before it reaches the LLM.

Further reading

22 / 41

The Next AI R&D Organization: From Functional Roles to Project Cells

AI will not erase expertise, but it will weaken fixed role boundaries. The basic delivery unit can shift from functional queues to small project cells with the capability to validate and own market outcomes.
Aug 20, 2026 · 14 min read

Traditional product and engineering organizations are divided by profession. Product defines requirements, design creates interfaces, frontend builds screens, backend owns services, QA verifies the release, and operations deploys it. Every role can be highly capable, and the process can look reassuringly complete.

But as markets move faster and AI reduces the cost of prototyping and implementation, the constraint changes. The important question becomes: how many queues, translations, handoffs, and reconfirmations must a real customer signal cross before it becomes a product that can be tested?

If the answer is still a long functional pipeline, AI may accelerate every local step without helping the organization reach evidence any sooner.

I believe the basic delivery unit of an AI-era R&D organization is shifting from frontend, backend, or product departments to small, market-facing project cells. One practical cell might combine a salesperson or business partner close to customers with two product-engineering generalists. Together they own the loop from problem discovery and solution design to delivery and feedback.

When the response is strong, they keep investing. When it is weak, they revise the hypothesis, change the product form, or stop and move to a better direction.

An AI-era R&D organization evolving from functional queues into a three-person project cell driven by market feedback

Expertise remains; waiting behind role boundaries does not

“No frontend or backend roles” can sound like expertise no longer matters or everyone must master every discipline. That is not the useful interpretation.

What should disappear is waiting justified by job boundaries: the request has not reached my queue; this is not my stack; the page is done but the API must wait; the feature shipped so the market result is somebody else's concern.

Deep expertise remains essential for security, data, architecture, performance, and complex interaction. The change is that expertise becomes a capability a project cell can invoke instead of a fixed station where work waits.

A generalist may use AI to build a prototype, interface, service, and baseline tests. High-risk code still receives specialist review. The cell can deliver end to end, while platform capabilities continue to enforce shared standards.

The more precise principle is:

A job title should not decide where work stops. The outcome should decide how far the team carries it.

Put differently: expertise remains, but it no longer needs to take the form of departmental walls.

From functional queues to project cells

Functional organizations allocate work through sequential queues. Project cells start with a testable business problem rather than a predetermined feature list.

A minimal cell can contain:

MemberCore contributionShould not be limited to
Sales / businessCustomer signal, context, validation access, commercial feedbackPassing requests downstream
Product-engineering member AHypothesis, interaction, end-to-end implementation, observationDocuments or frontend only
Product-engineering member BEngineering, integration, quality, delivery, runtime feedbackOne layer of code only

“One salesperson and two builders” is an illustrative pattern, not a universal staffing formula. An experience-heavy product may need design engineering; a data product may need an ML or data specialist; a mature product may include operations or customer success. Headcount and titles matter less than whether the cell has the minimum capabilities and authority to complete a discover-build-deliver-validate loop.

It cannot require a fresh resource negotiation at every step, and it cannot treat deployment as the end of the work.

The group owns an outcome: sustained use, a problem demonstrably solved, a pilot that can expand, or an improved commercial result. Code volume, feature count, and on-time delivery remain process signals, not the final result.

The operating loop

The cell should begin with a signal, not a specification:

Real customer signal
        ↓
Define the problem and smallest hypothesis
        ↓
Choose the lowest-cost product form
        ↓
Build and deliver a testable version
        ↓
Observe usage, value, and commercial evidence
        ↓
Iterate / revise / change form / stop

Start with customer reality

A customer asking for a dashboard may actually need earlier anomaly detection. A request for a chatbot may reflect fragmented information, slow decisions, or expensive service. Sales supplies context and access; the whole cell separates the problem from the proposed solution.

Sales brings back an unvalidated problem signal, not a requirements list.

Write a falsifiable hypothesis

The group states whose problem it is solving, why that problem matters, and what evidence would support or reject the direction. The smallest hypothesis is not a shorter PRD. It is a judgment that can be proven wrong.

Let the product form serve the test

The first version might be a service supported by an internal tool, a plugin inside an existing workflow, an automatically generated report, a conversational entry point, or an agent workflow that still requires human confirmation.

AI makes prototypes operational rather than merely presentational. Small teams can put interfaces, data, and partial automation into real tasks and observe behavior instead of collecting opinions about mockups.

Treat launch as the beginning of validation

The cell must see how the product is used, where people need explanation, whether they return, whether sales becomes easier, and whether delivery cost grows linearly with usage. Feedback passed through layers is not a closed loop.

Decide explicitly

Continue, pivot, and stop conditions should not be invented after results arrive. At kickoff, the cell defines a validation window, cost ceiling, required evidence, and decision point. After each cycle, it chooses one path against those standards:

  • Continue: problem, usage, and value signals are strengthening.
  • Revise the hypothesis: the problem exists, but the user, context, or value judgment was wrong.
  • Change the product form: the problem is real, but the current delivery method is too costly or difficult to adopt.
  • Stop: evidence remains weak, and more features would only hide a bad direction.

Stopping is a valid reallocation of scarce capacity. The expensive failure is continuing because the organization has already invested.

A project cell needs both the authority to start quickly and the discipline to stop in time.

AI increases capability density

Project cells do not work because an org chart became flatter. They work because AI expands the range each person can cover.

Product judgment can become an interactive prototype faster. Engineers can cross interface, service, and data boundaries. Sales can structure interviews and feedback. QA can generate boundary scenarios earlier. Agents can assist with research, code, analysis, and release preparation.

The reliable division of responsibility is:

  • AI expands, generates, organizes, and executes;
  • people own goals, authoritative facts, risk boundaries, and key judgment;
  • automated tests, review, permissions, and release mechanisms protect quality;
  • the project cell owns the business result.

The real value of AI is not making ten existing roles individually faster. It is enabling a smaller team to complete a full learning loop.

Remove functional walls, not quality boundaries

Reducing handoffs must not mean removing constraints. Cells can make most daily decisions autonomously, while the organization provides shared capabilities for identity, data access, audit, continuous delivery, observability, rollback, incident response, common components, model gateways, security, privacy, and compliance.

These capabilities should be productized as internal platforms rather than recreated as approval queues. A platform team helps every cell move safely; it does not take ownership of each project.

Project cells create speed at the edge; shared platforms preserve reuse, quality, and safety underneath.

Management changes too

Managers move from assigning tasks and tracking utilization to defining direction, authority, constraints, and resource boundaries. They decide which problems deserve validation, what the cell can decide, what budget and time window apply, when escalation is required, and how conflicts between projects are resolved.

They also protect cells from a stream of unpriced interruptions. Every new priority must state what it replaces or delays. When everything is urgent, every validation loop slows down.

Management does not disappear. It shifts from managing motion to managing context, so teams can make informed decisions and the organization can see risk, cost, and results.

Evaluation must change with it. If teams are still rewarded for requirements completed, code produced, or launches delivered on schedule, project cells will collapse back into task execution. Better measures include time from signal to testable product, learning gained per cycle, movement in user behavior and value, timely stopping of weak directions, and reusable capabilities created without compromising quality.

Evidence for continuing

Metrics vary by product, but every project should examine four kinds of evidence:

DimensionQuestion
Problem strengthDoes the problem occur often enough that users seek a solution?
Usage behaviorDo users complete the core action and return voluntarily?
Value resultDoes the product reduce cost, grow revenue, lower risk, or improve a critical experience?
Commercial evidenceWill customers pay, renew, recommend, or expand usage?
Delivery economicsCan the product scale, or does every new customer require equivalent manual effort?
Strategic valueDoes the work create reusable capability or open an important market entry point?

Trial does not imply retention. One sale does not imply repeatability. Frequent use can still be supported by unsustainable manual work. Cells should agree on evidence before the test, then update their judgment as reality arrives.

Feedback cycles also differ by product. A click, a compliment, or a single trial can be noise; enterprise procurement, complex workflows, and infrastructure products often require longer observation. Fast validation does not mean premature judgment. It means obtaining reliable evidence at the lowest cost appropriate to the product cycle.

This model is not for every kind of work

Project cells fit work with high uncertainty, direct access to real users, and a need to test product forms quickly: new-product discovery, AI application experiments, new-market pilots, and rapidly changing customer problems.

They should not be copied unchanged into every system. Core transactions, infrastructure, regulated domains, and high-security environments still require stronger separation of duties, specialist review, and change control. A cell operating there needs higher standards for automated verification, audit, and release.

Three questions help determine fit: Is the problem still highly uncertain? Can the cell obtain direct, authentic feedback? Can failure be recovered within a controlled cost? If most answers are no, the priority may be reliability, correctness, and scale efficiency rather than faster experimentation.

Common failure modes

Sales becomes the requirements commander

Sales is closest to customers, but a customer's proposed solution is not automatically the product answer. Sales supplies evidence and validation access; the cell defines the problem and response together.

Two builders inherit unlimited responsibility

A small team cannot own an oversized project indefinitely. Cells require narrower problems, strong platform capabilities, and explicit stopping rules. Otherwise, agility becomes exhaustion.

Role removal eliminates specialist review

Cross-stack delivery does not justify sending every change to production without review. More frequent releases demand stronger automated gates and risk-triggered specialist attention.

Every cell rebuilds infrastructure

If identity, data, models, monitoring, and deployment are rebuilt for each project, more cells create more debt. Shared capabilities belong in platforms, not another ticket-taking department.

The organization rewards success but punishes stopping

When stopping means failure, teams add features to defend the original direction. Good validation includes proving cheaply that a path is not worth pursuing.

Start with one experiment

Do not begin by announcing the end of frontend and backend titles. Select a bounded, low-risk problem with access to real users. Form a minimal cell, provide a clear outcome, a validation window, necessary authority, and platform support.

Ask the cell to deliver evidence, not merely features. Then review how long it took to move from signal to testable product, how much waiting disappeared, which specialist capabilities were still necessary, whether the team can explain continue/change/stop decisions, and whether the model can be repeated without sacrificing quality.

Only after the loop works should the organizational structure follow.

The basic unit should be a result loop

Industrial organizations gained efficiency through specialization. Internet companies used cross-functional teams to shorten delivery. AI further reduces the cost of acquiring knowledge, building prototypes, and executing across the stack, allowing smaller teams to carry more complete responsibility.

The central question for a future R&D center is no longer how many frontend, backend, product, and QA roles it contains. It is how many high-quality market validation loops it can run at once.

Expertise, platforms, and governance remain. But capabilities should move around project cells instead of projects moving through functional departments.

The goal of a small team is not to make fewer people produce more features. It is to let the people closest to a problem complete one real test at the lowest responsible cost.

The evolution is not about turning everyone into a directionless “full-stack individual.” It is about combining a few complementary people into a complete learning loop. Expertise remains while fixed handoffs disappear; platforms remain while departmental walls become thinner; managers move from distributing tasks to configuring goals, authority, resources, and stopping conditions.

When one business partner and two product-engineering members can discover, build, deliver, observe, and decide whether to continue, change, or stop, AI becomes more than an individual productivity tool. It becomes infrastructure for a different kind of organization.

Practices from internet and AI companies

This model is not an organizational label invented from scratch. AI changes the economics of cross-disciplinary execution, allowing smaller teams to own more of the loop from customer signal to product outcome. These three first-party practices support the article's central themes of live feedback, small-batch iteration, and end-to-end accountability.

  • OpenAI: Building OpenAI with OpenAI: OpenAI describes GTM, product, and engineering teams studying real workflows together, defining success, and testing changes in live deployments—closely matching a project cell that connects business signals directly with product iteration.
  • Google DORA: Working in Small Batches: small batches shorten feedback loops and make hypotheses easier to test and revise. DORA also connects this capability with turning AI-enabled development speed into product and organizational performance rather than additional friction.
  • Amazon AWS: Two-Pizza Teams and end-to-end ownership: the point of a small team is not size alone, but embedded resources, autonomy, accountability, and ownership of the end-to-end customer experience.

23 / 41

Software Is Becoming Disposable: How AI Rewrites SaaS and Internal Systems

As software becomes cheaper to generate and rewrite, companies no longer need a permanent SaaS product for every local need. More internal software will be contextual, short-lived, and continuously rebuilt—and startups will need revenue earlier.
Aug 20, 2026 · 13 min read

For two decades, the software industry rested on a stable assumption: software was expensive to build and even more expensive to maintain, so companies should buy standardized products and share development cost across many customers.

That was the economic foundation of SaaS. A vendor built once and sold subscriptions repeatedly; customers traded some customization for lower cost, faster deployment, and continuous upgrades.

AI changes that assumption. Natural language can now produce interfaces, services, data transformations, and automated workflows. A domain expert working with AI can create in hours or days what once required a project team. Software gains a rare new property: it can be generated quickly, then discarded or rewritten just as quickly.

I call this disposable software.

“Disposable” does not mean deleting production systems every day or ignoring quality, security, and data. It describes a new cost structure. For many local, low-risk, fast-changing needs, software no longer has to be treated as a five-year asset designed for everyone. It can serve one team, one customer, one campaign, or one decision cycle. When the need changes, rebuilding may be cheaper than preserving layers of compatibility.

A shift from standardized long-lived SaaS to software generated, validated, and rewritten around company context

SaaS's real competitor is not another SaaS product

Traditional SaaS competition is framed around features: more capabilities, a better interface, more integrations, or a lower price.

After AI, the real competitor may be the customer's own ability to build.

An internet company once bought separate systems for tickets, approvals, dashboards, sales assistance, content operations, and knowledge management. Every purchase required selection, contracts, implementation, training, permission configuration, and migration. Even when deployment succeeded, the company adapted its work to the software's predefined model.

In the future, companies may retain a small number of stable systems of record—finance, people, customer master data, and core transactions—then generate many lightweight applications above them:

  • a customer workspace for one sales campaign;
  • a budget and resource flow for a new project;
  • a campaign tool needed for only three months;
  • a delivery and analytics interface for one customer type;
  • an operating dashboard rewritten as strategy changes;
  • a temporary diagnostic tool for an exceptional process.

These tools do not need to become standalone SaaS products or satisfy every company's shared requirements. They need to understand one organization's data, permissions, language, and workflow, then perform reliably for a limited lifetime.

SaaS is no longer competing only with peer vendors. It competes with whether customers can now build something good enough themselves.

Why generic SaaS gets harder

SaaS will not disappear, but thin SaaS will struggle—especially products that wrap a simple workflow in a generic interface, rely primarily on per-seat pricing, and own no critical data or transaction entry point.

Features are easier to reproduce

A niche capability once required months of engineering and created a meaningful product window. Interfaces, CRUD operations, reports, notifications, permissions, and baseline AI can now be assembled rapidly. Features still matter, but features alone make a weaker long-term moat.

Standardization can become an extra cost

To serve more customers, generic SaaS accumulates configuration, roles, templates, and compatibility paths. Customers receive a system that can fit many companies, but not necessarily one that fits their work.

When customization becomes inexpensive, standardization is no longer automatically cheaper. Process change, training, and configuration maintenance are costs too.

Seat pricing conflicts with agentic work

Software charged per person because each person logged in and operated it. When agents execute work under the supervision of fewer people, value moves from access to completed outcomes. Pricing every human seat becomes harder to justify.

Buyers recalculate build versus buy

Companies will compare three years of subscriptions, implementation, and migration with building on top of existing data and model capabilities. This does not mean they always build. It means SaaS must create value clearly beyond the AI-assisted internal alternative.

When I say “SaaS cannot raise,” I do not mean there is literally no money

When I say SaaS companies increasingly cannot raise, I obviously do not mean investors have stopped funding software altogether. The money is still there, and AI companies are still receiving very high valuations. But for most ordinary SaaS founders, fundraising is no longer a path they can assume will be available.

The old habit was to build a product, tell a growth story, raise a round, and use the next round to keep searching for the real business model. That path is becoming much harder. The important change is not whether capital exists, but why anyone should give it to you.

A generic SaaS company once raised on market size and a growth story, then searched for durable unit economics later. Today, lower development barriers, crowded categories, and expanding model platforms make investors ask earlier:

  • Why can the customer not build this internally?
  • Why will a model platform not include it?
  • What proprietary data, distribution, or transaction relationship exists?
  • Does growth require continuously buying traffic and adding sales headcount?
  • How much gross margin is consumed by inference, human service, and custom delivery?
  • If financing stops, can customer revenue keep the company alive?

What I actually mean is: generic SaaS without real revenue, differentiated assets, or a way to survive beyond the next round will become increasingly difficult to keep alive.

Funding can accelerate a business model that already works. It should not be the business model itself.

SaaS startups must relearn how to support themselves

AI lowers product development cost and therefore lowers the necessity of outside capital. What once required a large team and millions of dollars may now be tested by three to five people.

The first objective can be revenue again: find a specific and painful problem, earn payment, cover people and model cost, then decide whether to expand.

Self-funding does not mean refusing capital. It turns funding from a survival requirement into a strategic choice:

  • the company can continue serving customers without a round;
  • capital accelerates growth that has already been validated;
  • free-user counts do not hide a lack of willingness to pay;
  • feature expansion is not used to prove the product still matters;
  • the next fundraising deadline does not force an unnatural scale target.

As software gets cheaper, small and profitable is not a consolation prize. It may be a healthier company design.

Internal software moves from project to capability

Traditional internal systems are built as projects: approval, requirements, scheduling, development, acceptance, launch, and a long maintenance tail. The system inventory grows while the organization becomes increasingly afraid to change it.

AI-native internal software behaves more like a continuous capability. Business people describe the problem, product-engineering defines boundaries, agents read approved company context and generate a working flow or interface, and the team validates it in real use before modifying or rebuilding it.

The future enterprise stack may have three layers:

LayerTypical contentsLifecycle
Stable systems of recordIdentity, finance, customers, contracts, transactions, core dataYears; tightly governed
Shared capability layerPermissions, model gateway, data APIs, audit, deployment, componentsContinuously evolving
Situational application layerWorkspaces, approvals, analysis, operations tools, customer-specific flowsDays to months; rebuildable

The third layer is what becomes disposable—not the ledger, permissions, or core data.

The stronger the stable foundation, the faster situational applications can change. Enterprise investment shifts from buying more interfaces toward governing data, permissions, APIs, models, and institutional knowledge.

Custom no longer has to mean outsourced customization

Custom software once implied a costly outsourced project: thick specifications, long delivery, unmaintainable code, and a new quote for every change.

AI-enabled customization is different. It looks like the organization continuously writing software for itself:

  • using its own data semantics and permission model;
  • involving the people who actually use the tool;
  • reusing an internal platform instead of restarting every time;
  • controlling risk through automated tests, audit, and deployment;
  • allowing low-value tools to retire naturally instead of maintaining them forever.

Professional engineers still own architecture, platforms, security, and high-risk systems. But many situational tools will be produced by domain experts, product engineers, and AI together.

Code is no longer the only scarce input. Domain understanding, reliable data, clear accountability, and judgment about what deserves to exist become more scarce.

Disposable must not mean software garbage

As generation gets cheaper, software sprawl becomes the obvious new failure mode. The ability to generate a tool does not grant it permission to touch production data or exist indefinitely.

Companies need boundaries:

  • Data: minimum access required for the task;
  • Identity: no bypassing centralized login, permissions, or offboarding;
  • Release: checks proportional to production risk;
  • Audit: records of who initiated important actions, what the model did, and what happened;
  • Ownership: an owner, usage scope, and retirement condition for every tool;
  • Reuse: repeated capabilities move into the platform rather than being copied dozens of times.

Disposable should mean inexpensive iteration and exit—not low quality, invisibility, or missing accountability.

Which SaaS products retain durable value

AI will not bring all software inside the enterprise. Several SaaS categories may become more valuable.

Systems of record and transaction networks

Finance, payments, contracts, identity, supply chain, and industry transaction systems represent trusted records, rule enforcement, and external networks—not merely an interface.

Deep industry data and decisions

Products that understand domain semantics, accumulate proprietary high-quality data, and influence consequential decisions are difficult to replace with a temporary generated app.

Security, compliance, and accountable delivery

When customers buy auditability, compliance, reliability, and responsibility—not just features—a specialist vendor can retain economies of scale.

Foundations for customer-generated software

The most valuable SaaS products may stop prescribing a fixed interface. They will provide APIs, data models, permission systems, agent capabilities, and composable components on which customers generate their own way of working.

SaaS does not disappear. It shifts from selling a fixed application to providing trusted capabilities that customers cannot easily reproduce.

What this means for companies and founders

Companies should not treat AI adoption as one more software subscription. The deeper task is developing internal software-generation capability: organizing data and APIs, building a safe foundation, pairing domain and engineering judgment, and letting low-value tools exit quickly.

Founders should no longer assume that implementing a feature creates a company. They must answer earlier why customers will keep paying, what cannot be reproduced by model platforms or internal teams, and whether the company can operate without the next round.

Product and engineering work also shifts from completing assigned modules toward understanding problems and delivering outcomes. As code gets cheaper, problem definition, systems judgment, data governance, and commercial understanding get more expensive.

Value moves from code to context

Industrialized software sought to build once and copy infinitely. Disposable software seeks to make each generation fit the current problem.

Companies will keep buying software, but they will question standardized products that contain interfaces and workflows without differentiated data or network value. They will preserve stable foundations while generating more short-lived, contextual applications internally.

Startups can no longer assume financing is the default fuel. Customer revenue remains the most reliable capital, and the strongest moats are no longer code volume but real workflows, trusted data, distribution relationships, and accountable delivery.

When software can be rewritten at any time, the assets worth accumulating are no longer every line of code, but context, data, trust, and judgment.

Disposable software is not the end of software value. It is a chance to turn a large class of software from assets that demand permanent care into tools that appear with a business need and retire gracefully when their work is done.

References

24 / 41

One Content Source, Three Reading Experiences: Web, WeChat Mini Program, and macOS

How I turned one source of technical notes into a responsive website, a WeChat Mini Program, and a native macOS reader through separate read-only APIs and platform-specific experiences.
Aug 20, 2026 · 9 min read

I have gradually expanded my personal website into three real reading products: a desktop and mobile website for public access and search, a WeChat Mini Program for quick discovery inside WeChat, and a native macOS application for focused reading, bookmarks, and offline access.

They are not three separately maintained content libraries, and the clients are not differently sized web wrappers. Each article is maintained once in the website repository. Purpose-specific read-only APIs then deliver the appropriate representation to each client. The facts are shared; interaction, information density, caching, and product responsibilities remain platform-specific.

Try the products
Visit houhuiyang.com · Download Code & Product Notes for macOS 1.0.0 (macOS 14+)
SHA-256: 7a45940a1278f366f452467b766372e5a693f1d144b0be525c902461d38511e9

Why build three versions?

The same reader has different needs in different contexts. In a desktop browser, someone may arrive through search and expect complete articles, project context, and shareable URLs. Inside WeChat, the priority is opening quickly, understanding the essentials, and forwarding a useful page. At a Mac, a reader may want search, bookmarks, reading progress, and access to recently opened articles when the network is unreliable.

ClientPrimary jobContent formKey experience
WebsitePublic publishing and durable discoveryComplete Chinese and English contentSEO, sharing, responsive layout
WeChat Mini ProgramFast discovery inside WeChatChinese summaries and selected sectionsQuick launch, article and project navigation
macOSFocused desktop readingComplete MarkdownSearch, bookmarks, book spread, progress, offline cache

Website: the source of truth and the public entrance

The website owns the content. Markdown files, titles, descriptions, dates, tags, covers, and bodies live in one versioned repository. Next.js produces the localized pages, metadata, sitemap, and stable article routes.

The desktop experience carries the complete information architecture: what I am doing now, the systems I use, how I think, the path I have taken, practical AI work, and technical notes. An article therefore sits inside a broader record of long-term work rather than becoming an isolated fragment.

Full desktop version of houhuiyang.com

Desktop homepage preview. Click the image to open the complete capture.

The mobile website uses the same content but reorganizes it for a narrow viewport. Navigation becomes a mobile menu, horizontal cards turn into a vertical flow, and headings, actions, and timelines gain a different rhythm. A reader opening a link from search or chat does not need to install anything.

Full mobile version of houhuiyang.com

Mobile homepage preview. Click the image to open the complete capture.

The website retains the highest information completeness and owns public URLs, canonical metadata, Open Graph previews, the sitemap, and indexing. Native clients improve specific reading situations without replacing the open web.

WeChat Mini Program: shortening the discovery path

The Mini Program is designed for quick entry from a conversation, QR code, or profile. Its home screen keeps only an introduction, current interests, selected projects, and article access. A fixed tab bar makes Home, Articles, and Projects the three primary destinations.

Code & Product Notes WeChat Mini Program home

The Mini Program does not parse website HTML. It calls the independent /api/articles interface, which returns titles, descriptions, tags, reading time, and selected sections suitable for a lightweight client. The app renders content but cannot edit the source.

This boundary keeps website redesigns from breaking the Mini Program and lets the API deliberately control information density. The public site supports complete expression; the Mini Program supports low-friction discovery.

Scan with WeChat

Scan to open the Code & Product Notes Mini Program

macOS: turning articles into a desktop book

The macOS version is a native Swift 6 and SwiftUI reader rather than a WebView shell. Profile navigation sits on the left, a searchable article library occupies the middle, and a two-page book layout renders the selected article on the right.

Code & Product Notes macOS library and book reader

The desktop client uses the /api/reader namespace for profile sections, article lists, full Markdown, tables of contents, and public image resources. It is intentionally separate from the Mini Program API: one client needs compact summaries, while the other needs complete content and durable reading state.

The current release supports:

  • Chinese and English articles and profile sections;
  • title and tag search;
  • bookmarks and local reading progress;
  • recent-content caching and offline fallback;
  • Markdown, tables, blockquotes, code blocks, and public images;
  • a two-page layout inspired by printed books.

Download the macOS app

Download Code & Product Notes 1.0.0 DMG

It requires macOS 14 Sonoma or newer. The current package is ad-hoc signed. If macOS blocks the first launch, right-click the app in Finder and choose Open, or confirm it under System Settings → Privacy & Security.

Verify the download with:

shasum -a 256 houhuiyang-notes-macos-1.0.0.dmg

Expected digest:

7a45940a1278f366f452467b766372e5a693f1d144b0be525c902461d38511e9

How one article reaches three clients

Markdown articles and profile data
              │
              ▼
      Next.js content layer
       │          │
       │          ├── Public website: complete pages, SEO, sharing
       │
       ├── /api/articles: Mini Program summaries and selected sections
       │
       └── /api/reader: macOS Markdown, table of contents, and assets

Every client API is read-only. Articles are created, changed, and removed only in the content repository. Bookmarks, progress, and caches belong to the local experience and never become article facts.

Stable identifiers also reduce coupling. Clients can use slug + locale + updated as a cache version key, ignore unknown future fields, and fall back to the most recent successful cache when the service is unavailable. The website, Mini Program, and macOS app can evolve at different speeds without duplicating ownership.

What I learned

First, multiple clients do not require multiple content sources. A single owner and deliberate contracts let one new article reach every reading surface.

Second, responsive websites and native clients solve different problems. The web offers the lowest barrier and greatest openness. The Mini Program shortens discovery inside WeChat. The macOS app uses screen space, local storage, and native interaction to preserve deeper reading state.

Third, the APIs exist to stabilize product boundaries. The Mini Program should not depend on website DOM, and the desktop reader should not copy the article database. Contracts let clients share facts while keeping their own product decisions.

I want Code & Product Notes to become more than a portfolio page. It is a small system for sustained writing, publishing, reading, and iteration. The website, Mini Program, and macOS app are its current entrances; the durable asset is the unified, portable body of work behind them.

25 / 41

A Vertical AI Model Is a System: Building Atlas, a Local AI Career Assistant

How I combined Qwen3-0.6B, MLX LoRA, a local job index, resume parsing, deterministic rules, intent routing, and release gates to build Shixiseng Atlas.
Aug 18, 2026 · 13 min read

Many products are marketed as a “legal LLM,” “medical LLM,” or “recruitment LLM.” The label can suggest that the team trained an entirely new foundation model from random weights.

That is rarely how practical vertical AI products are built.

Most of them place a general-purpose foundation model inside a larger domain system. Vertical data contributes domain behavior. Retrieval and databases provide facts. Rules handle deterministic decisions. Tools perform actions. Routing selects the right capability. Permissions and evaluation define what the model may see, what it may do, and whether a result is reliable enough to ship.

Shixiseng Atlas, the local AI career system I have been building, follows this pattern.

Atlas answering “Who are you?”

Atlas is not a new foundation model trained from scratch. Its current development version uses Qwen/Qwen3-0.6B-MLX-4bit as the base, applies an MLX LoRA adapter trained on recruitment relevance data, and combines it with a local job snapshot, resume parsing, retrieval and ranking, eligibility rules, and task workflows.

The central engineering question was not how to put a recruitment-themed interface around a chatbot. It was this: how do we let the model handle understanding and explanation while assigning facts, permissions, and execution to more dependable components?

A vertical LLM starts as a system, not a weight file

A practical vertical AI product usually looks like this:

General-purpose foundation model
+ domain prompts or LoRA fine-tuning
+ enterprise knowledge and operational databases
+ retrieval and ranking
+ deterministic rules
+ tools and task workflows
+ intent routing
+ permission and safety controls
+ offline evaluation and release gates

The model weights are only one layer.

If an LLM is responsible for job counts, degree constraints, relevance ranking, explanations, and the final application action, it can easily invent facts and becomes difficult to audit. Fluent generation does not provide transactional consistency, and confidence in wording cannot prove that an application actually succeeded.

Atlas therefore separates responsibilities:

CapabilityResponsible component in Atlas
Job counts and source fieldsLocal job database
Candidate job retrievalLocal vector index and keyword retrieval
Degree, city, and internship constraintsDeterministic eligibility rules
Resume-to-job relevanceRanking logic and the Atlas model
Match explanations and career adviceLocal language model
Resume visibilityContext permission controls
One-click applicationDeterministic workflow after user confirmation
Model release decisionFrozen evaluations and quality gates

The principle is simple: facts come from data sources, rules execute in code, and the language model handles understanding, synthesis, and explanation.

Atlas has an explicit, traceable identity

Atlas must be able to describe what it actually is instead of replying with a generic “I am an AI assistant.” The current identity includes:

  • model family: shixiseng-atlas;
  • development version: 0.1.0-dev;
  • base model: Qwen/Qwen3-0.6B-MLX-4bit;
  • adapter: mlx-qwen3-0.6b-relevance-balanced-v1;
  • training: MLX LoRA, currently trained for 192 iterations;
  • inference: local MLX and Metal on Apple Silicon;
  • data scope: local job snapshots, resumes, and conversations;
  • quality status: experimental-gate-failed.

The last item matters. A completed training run and a working inference path do not mean the adapter is production-ready. The current data scale, structured-output stability, and independent evaluation results are not sufficient to pass the release gate, so the product labels the model as experimental.

“Training completed” and “safe to ship” are different claims.

Three real application scenarios

Atlas currently supports three local workflows for job seekers. Every screen below comes from a real interaction with the experimental build rather than a design mockup or a static demo response.

1. Find opportunities in one city from a local resume

After selecting a resume stored on the machine, the user can ask Atlas to find opportunities in Shenzhen. Atlas retrieves and ranks records from the local job index, then reports the number of relevant and high-match opportunities together with inspectable match reasons.

Atlas finding Shenzhen opportunities from a local resume and explaining job matches

2. Compare opportunity volume across two cities

For a question such as “Does Shenzhen or Hangzhou have more opportunities?”, counts come from deterministic statistics over the local job index rather than an LLM guess. The response also states its counting scope so resume-keyword candidates are not mistaken for every job in each city.

Atlas comparing resume-relevant job counts in Shenzhen and Hangzhou

3. Compare cities and run a simulated application workflow

Atlas can retrieve matching jobs in two cities, generate a comparison, add the top option from each city to the conversation's application plan, and run a local simulation. The experimental build does not submit a real application to any employer; this workflow validates confirmation, task state, and result reporting.

Atlas comparing Shenzhen and Hangzhou opportunities and completing the local simulated-application workflow

Four intent routes instead of one universal chat path

Messages inside a career product represent very different tasks:

Find product internships in Beijing
Analyze my resume
How should I prepare for a product manager interview?
Name three famous Tang dynasty poets

Sending every message through one prompt while automatically attaching resumes and previous job results wastes context and creates privacy and task-contamination risks.

Atlas currently classifies requests into four routes:

Atlas end-to-end routing architecture, from user input to fact-checked output

RouteTypical taskReads resume?Job behavior
job_searchSearch, recommend, or compare jobsYesRuns a new local retrieval
resume_analysisAnalyze or improve a resumeYesDoes not automatically recommend jobs
career_chatInterview preparation and career adviceWhen neededReuses existing job context
general_chatNon-recruitment questionsNoDoes not read job context

The simplified view below shows how the four business modes and the safety fallback converge on a unified response layer:

Atlas simplified four-mode routing architecture

After routing, the system chooses the appropriate system prompt, determines whether the candidate profile may be attached, decides whether to access the local job index, and selects between a deterministic response and model generation.

For example, the model should not guess the answer to “How many suitable jobs are available in Beijing?” The system queries the local index for resume-relevant records first. The model may explain why those jobs matter, but it cannot rewrite the count.

Two contexts on one model—and the remaining limitation

The current Atlas MVP uses the same Qwen3 0.6B base and the same Atlas LoRA adapter for both general and career modes. Two system prompts and context isolation provide the behavioral split:

User message
→ intent classification
→ choose the general or career system prompt
→ decide whether resume and job context are permitted
→ answer through the local model or a deterministic bypass

This is inexpensive to build and simple to operate, which makes it appropriate for validating a local MVP. It already provides an important guarantee: general questions do not receive resume content, job lists, or job counts.

There is still a clear limitation: general mode changes the prompt but continues to load the recruitment LoRA. Domain behavior learned by the adapter can leak into otherwise general answers. A prompt can constrain the current task, but it cannot completely erase the adapter’s effect on model behavior.

The more mature design is task-aware adapter loading:

general_chat
→ general Instruct base
→ no recruitment LoRA
→ no resume or job access

resume_analysis / career_chat
→ the same compatible base
→ load Atlas LoRA on demand
→ read only authorized context

job_search
→ local job index and deterministic rules
→ model explains results only

application task
→ explicit user confirmation
→ deterministic business API
→ report the real execution result

This preserves shared tokenization, base weights, and inference infrastructure while preventing the recruitment adapter from continuously influencing general-purpose behavior.

Local execution is not merely a privacy slogan

Atlas keeps resumes, the job index, conversations, and model inference on the machine. Its macOS desktop application uses AppKit and WebKit as a lightweight native shell around the local service, while MLX loads and runs the model on Apple Silicon.

But “local” is not automatically equivalent to “safe.” The data flow still needs enforceable rules:

  • general mode must not read resumes or job context;
  • an unselected resume must not enter the prompt;
  • external links must not inherit local data;
  • applications require user confirmation;
  • generated text must never impersonate the result of a business API;
  • training, evaluation, and release artifacts need traceable versions and digests.

Privacy comes from a verifiable data flow, not from a “local AI” badge in the interface.

Why an experimental small model is still useful

Qwen3-0.6B is an experimental small model. Its knowledge coverage, complex reasoning, and instruction reliability are not comparable to larger production models.

It remains useful at this stage because it:

  • runs locally on ordinary Apple Silicon hardware;
  • keeps training and iteration costs low;
  • avoids sending resumes to an external inference provider;
  • forces the architecture to move facts and deterministic logic out of the model;
  • lets routing, permission, retrieval, and workflow interfaces survive a future base-model upgrade.

If a product works only when the model is treated as omniscient, its architecture is probably not reliable yet. If a 0.6B model can be limited to controlled language tasks while the system still returns real jobs, explainable matches, and safe actions, the product foundation is much more durable.

The current experiment and the production model plan

The Qwen3-0.6B-MLX-4bit + LoRA configuration shown in this article is only the current local experimental version. At this stage, the goal is not to claim that a 0.6B model already provides production-grade recruitment intelligence. It is to validate the complete engineering path at low cost: data preparation, LoRA training, model loading, intent routing, context isolation, local retrieval, deterministic rules, and quality evaluation.

The planned online version will use the more capable Qwen/Qwen3.5-27B + QLoRA. It will preserve the same system boundaries already tested here: job facts come from databases, hard constraints are evaluated by rules, the model focuses on semantic understanding, relevance analysis, and explanations, and high-risk actions require user confirmation and a deterministic workflow.

The current 0.6B version should therefore be understood as a runnable engineering validation environment, not the final model choice. It helps expose problems in data, training, routing, privacy, and evaluation early. Once the full path is stable, the project can invest the substantially higher training and inference cost in the online model without using a larger model to hide weaknesses in the surrounding system.

What Atlas taught me about vertical AI

A “vertical LLM” is more accurately a product system centered on a domain-adapted model without delegating every responsibility to that model.

Its defensibility does not come from one LoRA training run. It accumulates through the surrounding system:

  • lawful, clean, and evaluable vertical data;
  • retrieval and tools connected to real business objects;
  • explicit boundaries for deterministic rules;
  • least-privilege context access;
  • frozen evaluation sets and release thresholds;
  • visible failures instead of marketing labels that hide them.

The technically accurate description of Atlas is:

A recruitment-focused model system built on Qwen3-0.6B-MLX-4bit, adapted with Atlas recruitment data through LoRA, and combined with local job retrieval, resume parsing, eligibility rules, intent routing, and task workflows.

It is not a new foundation model trained from scratch, and it is not production-ready yet. But it validates a pragmatic direction: use a general model for language, vertical data for domain behavior, databases and rules for facts, and permissions, evaluations, and workflows to turn the model into a real product.

26 / 41

Building CoreX: A Native Real-Time macOS Monitor in Swift

How CoreX combines Mach, IOKit, Metal, SwiftUI, AppKit, and MenuBarExtra to monitor CPU, GPU, memory, disk, network, and thermal pressure entirely on the Mac.
Aug 17, 2026 · 13 min read

I built CoreX to answer a practical question: why does a Mac feel slow right now, why did its fans become audible, is memory pressure affecting the workload, and can those answers remain entirely on the device?

CoreX is a native, local-first macOS system monitor. Its interface is built with Swift 6, SwiftUI, and AppKit. Its sampler uses Mach, IOKit, Metal, Foundation, and Darwin to observe aggregate and per-core CPU load, GPU activity, memory, storage, network throughput, uptime, and system thermal pressure. The main window supports diagnosis, the menu bar supports quick awareness, and settings determine how the tool fits into everyday work.

Download CoreX 1.0 (macOS 14+, Apple Silicon)
Download CoreX-1.0-macOS-arm64.zip · approximately 2 MB
SHA-256: ae116fe638e81e3b2acfa34dd531ebbe4251bfa2b3cae7074a223136a7ea321a

CoreX system overview

Product boundaries: more data is not automatically better

System monitors tend to fall into two extremes. Some display a few percentages without enough context to explain a problem. Others expose every low-level counter and force the user to find the anomaly. CoreX uses three information layers:

  1. Overview: CPU, GPU, memory, storage, network, and thermal state for a fast health check.
  2. Diagnosis: per-core activity, P-core/E-core grouping, live histories, and device details.
  3. Ambient awareness: a small menu-bar surface for the metrics that matter most.

Sampling is separated from presentation. SystemMonitor produces a normalized snapshot; SwiftUI consumes state. Historical series are bounded rather than allowed to grow forever. Pausing monitoring stops sampling, not merely animation. This keeps collection, presentation, and preferences independently testable.

Mach / IOKit / Metal / Darwin / Foundation
                    │
                    ▼
             SystemMonitor sampler
       deltas, normalization, and clamping
                    │
          ┌─────────┴─────────┐
          ▼                   ▼
    current Snapshot      bounded histories
          │                   │
          └─────────┬─────────┘
                    ▼
       SwiftUI Dashboard + MenuBarExtra
                    │
                    ▼
      AppStorage / alerts / launch policy

Reading real macOS state from Swift

CPU: utilization is a delta between samples

CPU times are cumulative counters. Treating a single read as instantaneous load is incorrect. CoreX calls Mach APIs including host_statistics and host_processor_info for user, system, nice, and idle ticks, retains the previous sample, and calculates:

usage = Δ(user + system + nice) / Δ(user + system + nice + idle)

The same rule applies to aggregate and per-core load. On Apple Silicon, cores are grouped into performance and efficiency groups for presentation. A production implementation must handle the first sample, counter anomalies, allocated processor arrays, and value clamping to avoid misleading startup spikes.

Memory: pressure matters more than a scary percentage

CoreX uses host_statistics64 to read active, inactive, wired, compressed, and free pages. macOS deliberately uses spare memory for caching, so high usage alone is not proof of a problem. The interface combines the current value with history instead of turning one percentage red without context.

GPU: Metal identifies devices; IOKit attempts load sampling

MTLCopyAllDevices() supplies GPU identity and capabilities. Live utilization is read from IOAccelerator performance statistics through IOKit. This is not a uniformly documented, stable metric across every macOS and hardware combination. If a driver does not expose the necessary field, CoreX reports the metric as unavailable instead of inventing zero.

Storage, network, and thermal state

  • Storage uses URLResourceValues.volumeTotalCapacity and volumeAvailableCapacityForImportantUsage, which better reflects capacity available to important work.
  • Network sampling aggregates active interfaces through getifaddrs, excludes loopback, and derives upload and download rates from byte deltas. Interface switches and counter resets must be handled.
  • Thermal pressure comes from ProcessInfo.thermalState. It is an operating-system pressure signal—not a sensor temperature—so CoreX labels it nominal, fair, serious, or critical rather than fabricating degrees Celsius.

All readings are collected and rendered locally. CoreX requires no account and does not upload monitoring data.

Why SwiftUI still needs AppKit

SwiftUI is effective for dashboard cards, state-driven presentation, themes, and settings forms. A polished Mac utility also needs precise behavior for window activation, the Dock, application reopening, and the About panel. CoreX deliberately combines both frameworks:

  • WindowGroup owns the primary window and its sizing constraints.
  • MenuBarExtra(.window) creates an interactive status surface.
  • NSApplicationDelegateAdaptor controls termination after the last window closes and restores a hidden window when the Dock icon is clicked.
  • NSApplication.setActivationPolicy switches between a regular app and a menu-bar-only utility.
  • SMAppService manages launch at login.

SwiftUI describes the interface; AppKit closes the lifecycle gaps. That is more robust than forcing a real macOS utility into a single-framework ideology.

Settings are behavior, not decoration

CoreX settings

CoreX organizes preferences into Appearance, General, Monitoring, Notifications, Shortcuts, and About. Each setting is intended to connect to runtime behavior:

  • system, light, or dark appearance; accent, density, corner radius, glass, and motion;
  • launch at login, open-window or menu-bar startup, background operation, and Dock visibility;
  • 1/2/5-second intervals, enabled categories, and 60-second/5-minute/15-minute histories;
  • icon-only, CPU, or CPU-and-GPU menu-bar content;
  • CPU, memory, storage, and thermal-pressure notifications;
  • shortcuts for opening the overview and pausing monitoring.

@AppStorage persists preferences, but persistence is not implementation. A Dock setting must change the activation policy. A refresh interval must alter the sampling cadence. Closing behavior must reach the application delegate. I tested each preference as an end-to-end path from UI control to operating-system behavior.

The menu bar is the high-frequency surface

CoreX menu-bar status

The menu-bar panel deliberately limits itself to CPU, GPU, memory, current status, settings, update checks, and quit. It works well while compiling, running local models, processing video, or switching among heavy applications. The dashboard explains; the menu bar informs without interruption.

Development and packaging environment

CoreX uses Swift Package Manager, targets macOS 14, and links IOKit and Metal. The development machine should use a matched Xcode or Command Line Tools installation. Mixing a Swift compiler and macOS SDK from different toolchain releases can produce an “SDK compiled by a different version of Swift” failure. A dependable release pipeline pins Xcode and builds in a clean environment.

cd MacPulse
./scripts/build-app.sh
open dist/CoreX.app

The build script performs a release build, assembles the .app bundle, generates icons, and signs the result. The current download is an arm64, ad-hoc-signed build intended for evaluation and internal distribution. A broad public release should use a Developer ID Application certificate, Apple notarization, and a stapled notarization ticket.

Supported Macs

ItemCurrent download
Operating systemmacOS 14 Sonoma or newer
ProcessorApple Silicon: M1, M2, M3, M4, and M5 families
Intel MacsNot supported by the current arm64 archive; the source can be extended to a universal build
NetworkNot required during operation, except for update checks
Data handlingMonitoring data remains on the device
GPU metricAvailability depends on IOKit fields exposed by the system driver

CoreX is particularly useful for developers, local-LLM users, designers, video professionals, and anyone who wants ongoing visibility into a Mac. It is not a data-center monitoring system, a remote alerting service, or a substitute for Apple Diagnostics.

Download, installation, and security

  1. Download CoreX 1.0 and unzip it.
  2. Move CoreX.app to Applications.
  3. On first launch, right-click the app and choose Open. If Gatekeeper blocks it, confirm Open Anyway under System Settings → Privacy & Security.
  4. Verify the archive if desired:
shasum -a 256 CoreX-1.0-macOS-arm64.zip
# ae116fe638e81e3b2acfa34dd531ebbe4251bfa2b3cae7074a223136a7ea321a

This test build is not Apple-notarized. That limitation must be explicit; users should not have to discover the release model from an unexplained Gatekeeper dialog.

Engineering lessons

First, system monitoring is primarily a semantics problem, not a charting problem. Cumulative counters require deltas, missing metrics must remain missing, and thermal pressure must not masquerade as temperature.

Second, native quality comes from behavioral closure. Windows, Dock policy, the menu bar, login launch, and preferences become features only when they are wired into the application lifecycle.

Third, distribution is part of the product. Pinned toolchains, architecture labels, signing, notarization, hashes, and installation guidance belong in the design—not as release-day cleanup.

Next steps for CoreX include more resilient GPU compatibility, stronger memory-pressure diagnosis, process-level attribution, a universal binary, and full Developer ID signing and notarization. For a local system utility, trust, accuracy, and low interruption matter more than adding another dashboard.

27 / 41

FaceFizz: Building a Privacy-First Camera Inside the Browser

An engineering retrospective on FaceFizz, from camera permissions and real-time Canvas effects to local MediaPipe segmentation and explicit resource cleanup.
Aug 11, 2026 · 9 min read

I have made the source code for FaceFizz.fun publicly available on GitHub. FaceFizz is a browser photo booth: no app installation and no account are required. After granting camera access, a user can preview live warps, clones, stickers, frames, art filters, festival looks, and background scenes, then save a photo locally.

The product is compact, but its main path combines browser media APIs, real-time graphics, on-device AI, privacy boundaries, responsive design, localization, and discoverability. This article explains the decisions behind that path rather than repeating a feature list.

Start with the experience boundary

The value of a funny camera is not the number of filters. It is how quickly a first-time visitor reaches a result worth keeping. FaceFizz therefore follows several constraints:

  1. The camera comes before registration—there is no required account.
  2. The purpose of camera permission is explained, and the stream stops when the camera closes.
  3. Live frames and generated photos are not uploaded by default.
  4. The homepage presents eight curated entry effects instead of overwhelming users with quantity.
  5. Desktop supports exploration, while mobile keeps permission, effect selection, capture, and save within a simple one-handed flow.

These constraints lead to a local browser pipeline rather than an upload–process–download architecture.

The interface

The desktop homepage uses an editorial layout that combines effect categories, a preview mosaic, privacy messaging, and a clear camera action.

FaceFizz desktop homepage

The mobile version reorganizes information density instead of merely shrinking the desktop grid.

FaceFizz mobile homepage

The browser processing pipeline

Camera permission
  → getUserMedia MediaStream
  → live video frames
  → mirror and cover crop
  → Canvas 2D effect pipeline
      ├─ warps / mirrors / mosaics
      ├─ color / pixel / art filters
      ├─ stickers / frames / festival assets
      └─ MediaPipe segmentation → background compositing
  → requestAnimationFrame preview
  → countdown capture
  → JPEG export
  → local download

The browser requests the user-facing camera with an ideal 1280×1280 resolution. Effects are rendered into a consistent 720×720 Canvas. A fixed internal canvas simplifies differences in device video dimensions and lets the live preview and saved result share the same rendering path.

The render loop is driven by requestAnimationFrame. Frames are cover-cropped, mirrored through an intermediate Canvas when necessary, and passed to the selected effect. Capture does not reproduce those effects in a second implementation; it exports the current output Canvas as JPEG at quality 0.92. That avoids drift between what the user previews and what they save.

Why Canvas 2D was the right MVP choice

Canvas 2D covers the first product scope well: region stretching, slice displacement, mirrors, grids, pixelation, color treatment, image overlays, frames, and mask compositing. It is easy to debug, integrates naturally with image assets, and works across modern desktop and mobile browsers.

The trade-off is main-thread pressure. Complex mesh warps, high-resolution pixel work, and many compositing layers will eventually need WebGL2 shaders, Workers, and capability-based degradation. Canvas 2D is the right validation tool, not necessarily the final rendering engine.

MediaPipe enters only when needed

Background replacement uses MediaPipe ImageSegmenter and a locally hosted Selfie Segmenter model. Camera frames do not need to be sent to a remote inference API.

Initialization prefers a GPU delegate and falls back to CPU when GPU setup fails. Segmentation also does not run on every display frame. It is throttled to roughly 80 ms, and the latest mask is reused between inference passes. Separating display refresh from model inference reduces main-thread work and device heat.

Video frames ─────────────→ real-time Canvas rendering
      └─ about every 80 ms → ImageSegmenter → person mask
                                              ↓
background + source frame + mask → composite result

Edge refinement, hair detail, low-end latency, and custom-background memory use remain areas for further device testing.

Privacy is a resource lifecycle

“Processed locally” must be visible in code, not only in copy. FaceFizz currently:

  • requires no account and creates no cloud gallery;
  • keeps frames, masks, and capture results in the browser by default;
  • creates a JPEG only after the shutter action;
  • saves a file only after an explicit download action;
  • calls stop() on every MediaStreamTrack when the camera closes;
  • cancels animation work and closes the MediaPipe instance during cleanup.

This boundary also removes backend complexity: the MVP needs no face-photo storage, retention workflow, or gallery authorization model. Privacy engineering directly reduces system cost.

Type safety before plug-in architecture

The current implementation defines explicit CategoryId, EffectId, and Effect types. Categories, effect metadata, colors, and localized names are checked by TypeScript. Curated effects shape the homepage while broader categories live in the picker.

The honest limitation is that much of this logic still lives in one large client page, and renderer behavior is organized mainly through branches. That is efficient for validating the product, but it will not scale indefinitely. A declarative template protocol should eventually separate effect metadata, assets, capability requirements, and renderer selection:

type EffectTemplate = {
  id: string;
  version: number;
  category: "warp" | "sticker" | "frame" | "filter" | "background";
  preview: string;
  assets: string[];
  renderer: "canvas2d" | "webgl" | "segmentation";
  capture: { aspectRatio: "1:1" | "3:4" | "9:16" };
  capability: { webgl2?: boolean; segmentation?: boolean };
};

The page can then own interaction state, renderers can own execution, and templates can be registered and tested independently.

Seven languages and machine discoverability

FaceFizz supports Simplified Chinese, Traditional Chinese, English, Japanese, Korean, Thai, and Bahasa Melayu. Localization affects much more than strings: title length, card height, mobile wrapping, font fallback, and metadata all change.

The current typed dictionaries are appropriately lightweight for a single-page product. Growth would justify split language packs, lazy loading, missing-key checks, and screenshot regression tests. The project also publishes structured data, a sitemap, robots.txt, llms.txt, and llms-full.txt to make the product understandable to search engines and AI retrieval systems.

What should improve next

The project uses React 19, TypeScript, Vinext, Vite, Tailwind CSS 4, and MediaPipe Tasks Vision. ESLint, production builds, and rendered-HTML tests provide a baseline. The next engineering priorities are clearer:

  • split camera control, effect registration, renderers, and localization into modules;
  • build a real Chrome, Safari, iOS, and Android camera test matrix;
  • measure camera-start success, first-frame time, FPS, and capture success without collecting face content;
  • pause and resume work correctly when the page enters the background;
  • degrade segmentation and canvas resolution on constrained devices;
  • strengthen keyboard, focus-management, and screen-reader testing.

What I learned

On-device AI is not merely a way to avoid API cost. It shortens feedback, reduces network dependency, and creates a privacy boundary users can understand.

Real-time products also cannot be judged only by whether a feature works. Camera startup, first-frame latency, render FPS, inference cadence, heat, and cleanup together define the experience.

Finally, an MVP does not need the most advanced architecture. Canvas 2D, local inference, and a single-page state model are enough to validate the core experience—as long as the boundaries that must evolve are recorded honestly.

The real north-star metric is not page views. It is the number of people who successfully create, save, or choose to share a photo. Technology ultimately serves the moment that makes someone smile.

Try it and read the source

License note: as of August 11, 2026, the repository exposes its source but the README still says “All rights reserved” and no standalone open-source license is present. Strictly speaking, it is currently source-available rather than an OSI-licensed open-source project. Community reuse requires an explicit license such as MIT or Apache-2.0.

28 / 41

A Packaging Bug I Found in Homebrew Redis 8.10.0

A short debugging note on a Redis startup failure caused by enabled module directives whose libraries were missing from the Homebrew bottle.
Aug 11, 2026 · 3 min read

I recently installed Redis 8.10.0 with Homebrew for local development:

brew install redis
brew services start redis

The installation completed normally, and Homebrew reported that the service had started. Its actual state in brew services list, however, was:

redis  error  1

The problem was not that Redis itself could not run. The default configuration shipped in the bottle did not match the files included in that bottle. I reported the bug as Homebrew/homebrew-core#296698. As of August 11, 2026, the issue remains open.

The mismatch

The bottle-provided redis.conf enabled four modules:

loadmodule ./modules/redisbloom/redisbloom.so
loadmodule ./modules/redisearch/redisearch.so
loadmodule ./modules/redisjson/rejson.so
loadmodule ./modules/redistimeseries/redistimeseries.so

The installed bottle contained none of the referenced .so or .dylib files. Redis read the directives, failed to load the missing modules, and exited.

This is an easy failure to misread. “Successfully started” means the service command was submitted; it does not prove that the Redis process remained healthy.

Verifying the cause

I compared the configuration with the installed artifacts:

grep -n "loadmodule.*modules" \
  /opt/homebrew/Cellar/redis/8.10.0/.bottle/etc/redis.conf

find /opt/homebrew/Cellar/redis/8.10.0 \
  -type f \( -name "*.so" -o -name "*.dylib" \)

The first command returned four active loadmodule directives. The second found no corresponding module libraries. That configuration–artifact mismatch was the direct cause.

Temporary workaround

For local development that does not need these modules, the directives can be commented out before restarting Redis:

sed -i '' \
  -e 's|^loadmodule ./modules/|#loadmodule ./modules/|' \
  /opt/homebrew/etc/redis.conf

brew services restart redis
redis-cli ping

After the change, redis-cli ping returned:

PONG

This is a workaround, not an upstream fix. If an application depends on RedisBloom, RediSearch, RedisJSON, or RedisTimeSeries, disabling them is not an acceptable solution. In that case, use a distribution that explicitly includes the required modules or wait for the Homebrew bottle to be corrected.

The practical lesson

When a service fails to start, repeatedly reinstalling or restarting it rarely produces new information. A better sequence is:

  1. Check the real service state and exit code.
  2. Run the process in the foreground and read the direct error.
  3. Compare configuration declarations with installed artifacts.
  4. Make the smallest possible change to verify causality.
  5. Publish a reproducible report for the upstream maintainers.

Open-source contribution does not always begin with a code patch. A focused bug report with clear evidence and reliable reproduction steps can be just as useful.

References

29 / 41

Building a Recruitment LLM from Zero to One: A BOSS Zhipin–Style Case Study

A practical open-source blueprint for a recruitment-domain language model: product goals, data governance, continued pretraining, SFT and DPO, RAG, candidate–job matching, evaluation, safety, deployment, and a 12-week delivery plan.
Aug 10, 2026 · 24 min read

If I had to build a language model for a recruitment platform similar to BOSS Zhipin, I would not begin by buying GPUs or debating whether the model should have 7B, 32B, or 100B parameters.

I would begin with three questions:

  1. Which business outcome must the system improve?
  2. What lawful, distinctive, and verifiable data do we have?
  3. Which problems belong to an LLM, and which should remain search, recommendation, or rule-based problems?

The easiest mistake in recruitment AI is to treat “building an industry model” as training a chatbot. A production system is not one model. It is a coordinated system of data, retrieval, matching, generation, risk controls, evaluation, and human decisions.

One factual boundary matters. Public filing information lists “Nanbeige,” operated by the company behind BOSS Zhipin, under filing number Beijing-NanBeiGe-20240102. Its complete data recipe, training pipeline, and internal architecture are not public.Cyberspace Administration of China filing notice

This article therefore does not claim to reconstruct BOSS Zhipin's internal system. It uses a BOSS Zhipin–style platform as a business case and presents an implementable open-source reference architecture.

What “from zero” should mean

Pretraining a general foundation model from random initialization requires trillions of tokens, long-running clusters, mature distributed systems, and substantial compute. For almost every recruitment company, that is the wrong first milestone.

A practical zero-to-one path is:

open-weight base model
    ↓
recruitment-domain continued pretraining (optional)
    ↓
task-oriented supervised fine-tuning
    ↓
preference optimization and safety alignment
    ↓
RAG, business tools, and matching systems
    ↓
offline evaluation, canary release, and feedback loop

“Self-developed” does not have to mean random initial weights. The assets a company should own are its domain data, training recipe, evaluation sets, service architecture, and learning loop.

Training from scratch becomes reasonable only when the company has a very large lawful corpus, stable compute, an experienced training team, and a base-model deficiency in tokenization, language coverage, or core behavior that adaptation cannot fix.

Step 1: turn business goals into evaluable tasks

A recruitment platform serves both candidates and recruiters, whose goals differ.

Candidates need to know which roles genuinely fit, which requirements are hard constraints, how to improve a résumé, how to communicate effectively, and whether a role or company is trustworthy.

Recruiters need to turn ambiguous needs into clear job descriptions, discover qualified candidates, understand skill fit, reduce unproductive conversations, and identify fraudulent résumés, jobs, or accounts.

Version one should not solve everything. I would choose three measurable scenarios:

ScenarioOutputPrimary metrics
JD normalization and rewritingStandard title, skills, experience, location, compensation, and a faithful rewriteField F1, factual fidelity, publish acceptance
Candidate–job explanationMatches, gaps, evidence, and uncertaintyRecall@K, NDCG, evidence accuracy
Recruitment copilotQuestions, replies, and next-step suggestions grounded in the job and résuméReply rate, human acceptance, safety violations

Do not use hiring rate as the only early metric. Hiring is delayed and influenced by compensation, location, employer brand, recruiter behavior, and the economy. Early evaluation needs model-quality metrics, process metrics, and business outcomes together.

Step 2: separate the system instead of asking the LLM to do everything

A recruitment platform needs a layered architecture:

                        ┌─────────────────────────────┐
candidate / recruiter → │ chat, search, recommendation│
                        └──────────────┬──────────────┘
                                       ↓
                        ┌─────────────────────────────┐
                        │ task routing and policy      │
                        │ intent / auth / risk / cost  │
                        └───────┬──────────┬──────────┘
                                │          │
                  ┌─────────────┘          └──────────────┐
                  ↓                                          ↓
       ┌───────────────────────┐                ┌───────────────────────┐
       │ recruitment LLM       │                │ retrieval and ranking │
       │ extract/generate/explain│              │ two-tower/rank/rerank │
       └───────────┬───────────┘                └───────────┬───────────┘
                   ↓                                            ↓
       ┌───────────────────────┐                ┌───────────────────────┐
       │ RAG and business tools│                │ feature/vector platform│
       │ jobs/company/API      │                │ user/job/behavior      │
       └───────────┬───────────┘                └───────────┬───────────┘
                   └───────────────────┬────────────────────┘
                                       ↓
                        ┌─────────────────────────────┐
                        │ governance, eval, audit, loop│
                        └─────────────────────────────┘

LLMs are strong at semantic interpretation, extraction, generation, explanations, and tool orchestration. They are not the right engine for low-latency retrieval across millions of candidates, and they should not make opaque rejection decisions by themselves.

A mature design usually assigns:

  • search and two-tower models to high-scale retrieval;
  • ranking models to combine semantics, behavior, freshness, and constraints;
  • the LLM to parse natural language, enrich structured features, explain results, and conduct dialogue;
  • rules and safety models to hard constraints, authorization, fraud, and compliance;
  • humans to the final application, contact, interview, and hiring decisions.

A minimal open-source stack

Do not accumulate frameworks merely to claim an open stack. Give each layer one primary implementation and a replaceable boundary:

LayerInitial choiceOutput contract
DataPython/SQL + DataTrove or SparkVersioned Parquet, dataset card, lineage
TrainingPyTorch + LLaMA-Factory/TRL; FSDP or DeepSpeed when scale requires itAdapter/checkpoint and metrics
Lexical/vector searchOpenSearch, or PostgreSQL + pgvector initiallyFiltered candidates and evidence
Embedding/rerankingOpen-weight bilingual embedding + cross-encoderVectors and relevance scores
ServingvLLM or SGLangOpenAI-compatible API
Experiment/evaluationMLflow + private gold sets and regression scriptsVersioned data, model, prompt, metrics

Tool names matter less than interfaces. Training examples, embeddings, retrieval results, generations, and evaluations should carry dataset_version, model_version, prompt_version, and trace_id; otherwise production failures cannot be reconstructed.

Step 3: build a recruitment ontology first

More data creates more contradiction when terms do not share a common meaning.

“Senior Java developer,” “backend engineer,” and “server-side engineer” may belong to the same job family. “LLM experience” may mean calling an API, or it may mean training, evaluation, and inference optimization.

I would first build a versioned ontology:

job family → normalized title → specialization → skill → proficiency
                                                   ├─ required/preferred
                                                   ├─ years of use
                                                   └─ recency

job → industry → company stage → location → work mode → compensation

candidate → experience → project → responsibility → action → result → evidence

The ontology does not force every employer to use the same title. It gives retrieval, training, and evaluation a shared coordinate system. Every mapping should retain the original phrase, normalized value, confidence, and ontology version.

A structured JD example should include evidence:

{
  "source_text": "Hiring a senior backend engineer, 5+ years, Java and microservices required; LLM experience preferred.",
  "job_family": "Software Engineering",
  "normalized_title": "Senior Backend Engineer",
  "required_skills": ["Java", "Microservices"],
  "preferred_skills": ["Large Language Models"],
  "experience_min_years": 5,
  "evidence": {
    "experience_min_years": "5+ years",
    "required_skills": ["Java and microservices required"]
  },
  "confidence": 0.96,
  "ontology_version": "job-ontology-2026-08"
}

The important field is not JSON; it is evidence. Recruitment outputs must remain traceable to source text.

Step 4: create lawful, traceable data assets

Recruitment data is unusually sensitive. Résumés can contain names, contact details, education, employment history, location, and salary expectations. Conversations can reveal family circumstances, health information, and other private details.

I would separate data into four layers:

LayerExamplesMain use
Public knowledgeOccupational taxonomies, public skill documentation, labor rules, public company informationDomain knowledge and RAG
Platform dataAuthorized JDs, de-identified résumés, search and interaction dataContinued pretraining and matching
Expert labelsNormalized roles, skill evidence, fit judgments, preferred responsesSFT, preference optimization, evaluation
Online feedbackAccept, edit, reply, apply, interview, complaintIteration and impact measurement

Every record needs provenance, authorization basis, collection time, owner, sensitivity, permitted tasks, transformation history, retention, and deletion status.

The fact that data exists in a platform database does not automatically authorize model training. Product operation, recommendation, fraud prevention, and training are different processing purposes and require separate review.

Recruitment data pipeline

source registration and permission review
        ↓
parse JDs, résumés, conversations, and event logs
        ↓
remove templates, ads, contact solicitation, and corruption
        ↓
detect language, job family, industry, and content type
        ↓
detect PII, tokenize replacements, isolate identity mapping
        ↓
exact dedup, résumé-version dedup, fuzzy JD-template dedup
        ↓
detect fraud, low quality, discriminatory text, and conflicts
        ↓
quality scoring and evidence-completeness checks
        ↓
split by time, user, and company—not random rows
        ↓
dataset card, version, and audit record

Regular expressions are insufficient for de-identification. Emails and phone numbers fit rules; names, schools, project descriptions, addresses, and identity hints in free text need NER, dictionaries, model-based detection, and sampled review.

Random row splits are also unsafe. Multiple résumés from one person, templated JDs from one employer, or messages from one conversation can leak across train and test sets. Group by person and employer, split by time, and check contamination against public benchmarks.

Step 5: choose a base model by workload, not leaderboard rank

For Chinese recruitment, candidate open-weight families include Qwen, GLM, and Llama. The Qwen ecosystem supports Transformers, vLLM, SGLang, and fine-tuning frameworks such as LLaMA-Factory and TRL.Qwen official repository

Evaluate:

  1. License: commercial and derivative-model terms;
  2. Chinese and mixed-language quality: titles and technical stacks often mix Chinese and English;
  3. Structured output: JSON Schema adherence and field stability;
  4. Long context: full résumés, JDs, and conversations—not just advertised context length;
  5. Tool calling: argument accuracy, recovery, and authorization boundaries;
  6. Serving cost: TTFT, TPOT, throughput, and memory at target concurrency;
  7. Domain baseline: performance on the company's recruitment evaluation suite.

A 7B–14B model is often the right first experiment. Prove the task and data before scaling. Larger models are not automatically better for every classifier or extractor, and stable tasks can later be distilled into smaller models.

Measure the tokenizer on real JDs, résumés, and technical vocabulary: characters per token, truncation rate, and terminology fragmentation. Do not modify the vocabulary unless measured gains justify compatibility and retraining costs.

Step 6: train in stages

Stage A: continued pretraining (optional)

Continued pretraining teaches domain language and relationships using large amounts of unlabeled text. Candidate sources include authorized and deduplicated JDs, de-identified occupational material, public labor rules, high-quality industry documents, and strictly governed business text.

Do not dump all résumés and conversations into pretraining. Apply PII, safety, quality, duplication, and mixture controls first. Mix in some general data to reduce catastrophic forgetting, and use small ablations to determine whether continued pretraining is worth its cost.

If domain text is limited and the base model is already strong, SFT plus RAG is often a better investment.

Continued pretraining still optimizes next-token cross-entropy:

L_CPT = -Σ log Pθ(x_t | x_<t)

Four engineering controls matter:

  • Mixture: sweep candidate domain/general ratios such as 3:7, 5:5, and 7:3 in small runs instead of treating one ratio as law;
  • Boundaries: sequence packing improves utilization, but attention must not leak across documents;
  • Token budget: estimate effective tokens multiplied by cost per training token, not file size;
  • Stopping: monitor domain validation loss, general-capability regression, and target-task metrics together.

Each checkpoint needs a data manifest, code revision, random seed, optimizer state, and mixture. Falling domain loss with large general regressions indicates forgetting, not success.

Stage B: supervised fine-tuning

SFT should cover:

  • JD and résumé extraction;
  • skill and experience evidence;
  • search-constraint parsing;
  • fit explanations and gap analysis;
  • JD rewriting and résumé suggestions;
  • recruitment conversation and tool calls;
  • clarification and abstention when information is missing;
  • privacy, fairness, and safety cases.

A high-quality example includes not only a prompt and response, but task type, source evidence, allowed tools, expected output, risk labels, and dataset version.

{
  "messages": [
    {"role": "system", "content": "You are a recruitment assistant. Use only the supplied job and résumé. Never infer age, marital status, family plans, or health."},
    {"role": "user", "content": "Explain matches and gaps.\n[JOB]...\n[RESUME]..."},
    {"role": "assistant", "content": "{\"matched\":[...],\"gaps\":[...],\"unknown\":[...],\"evidence\":[...]}"}
  ],
  "task": "job_candidate_explanation",
  "risk_tags": ["employment", "personal_information"],
  "dataset_version": "recruit-sft-2026-08"
}

LoRA or QLoRA is appropriate for fast validation. LLaMA-Factory supports continued pretraining, SFT, DPO, and related workflows.LLaMA-Factory

model_name_or_path: Qwen/Qwen3-8B
stage: sft
do_train: true
finetuning_type: lora
lora_target: all
dataset: recruitment_sft
template: qwen3
cutoff_len: 4096
learning_rate: 1.0e-4
num_train_epochs: 2.0
bf16: true
val_size: 0.05
llamafactory-cli train recruitment_sft.yaml

These are starting parameters, not a universal recipe. Learning rate, epochs, sequence length, rank, and mixture require validation and ablation.

Several SFT details are easy to miss:

  • normally compute loss only on assistant tokens instead of teaching the model to repeat system and user text;
  • bucket and pack sequences to reduce padding while preserving conversation boundaries;
  • evaluate both token loss and schema validity for structured tasks—they do not always move together;
  • start LoRA experiments around rank 16 or 32 on attention and MLP linear layers, then expand through ablation;
  • maintain a blind test set never used during prompt iteration.

Whether full fine-tuning beats LoRA depends on dataset size, task breadth, and compute. First use LoRA to prove the data; run a controlled full-tuning comparison only when adapter capacity is a measured bottleneck.

Stage C: preference optimization

Once the model can perform the task but remains inconsistent in style, evidence, or refusal boundaries, collect preferred and rejected responses.

Prefer grounded explanations over unsupported judgments, uncertainty over fabricated experience, capability-related language over age or family proxies, concise action over empty prose, and valid tool calls over invented live job data.

DPO is simpler than traditional RLHF for an initial system. TRL provides SFT, DPO, reward modeling, and other post-training tools.Hugging Face TRL

Do not adopt GRPO simply because it is fashionable. Reinforcement learning is most dependable when rewards are verifiable—schema validity, SQL execution, code tests, or deterministic rules. Subjective employment judgments should never be reduced to one reward model.

DPO directly increases the relative probability of a preferred response over a rejected one:

L_DPO = -log σ(β[(log πθ(y+|x)-log πref(y+|x))
                 -(log πθ(y-|x)-log πref(y-|x))])

β controls deviation from the reference model. Bucket by task, length, and risk, and verify that “preference improvement” is not merely verbosity. When recruitment experts genuinely disagree, retain annotator distributions or send the case to review instead of inventing one truth.

Step 7: use RAG for changing facts and tools for actions

Job availability, compensation, employer information, and application status change continuously. They should not be memorized in model parameters.

The retrieval layer can include current jobs and versions, verified employer data, skill knowledge, policies, labor rules, and only the résumés or conversations the current user is authorized to access.

question
   ↓
intent and authorization
   ↓
structured filters: location, salary, experience, status
   ↓
hybrid retrieval: lexical + vector
   ↓
reranker
   ↓
evidence compression and citations
   ↓
generation or business-tool call
   ↓
factuality, authorization, and safety checks

Recruitment search cannot be vector similarity alone. Location, compensation, experience, job status, and access are hard filters. Semantic retrieval handles differences such as “AI platform engineering” versus “LLM infrastructure.” A reranker then scores a small candidate set.

Do not add raw BM25 and vector scores directly; they usually have incompatible scales. Reciprocal Rank Fusion is a robust first implementation:

RRF(d) = Σ 1 / (k + rank_i(d))

Retrieve lexical and dense Top-K lists separately, fuse their ranks, and send the best 50–200 records to a cross-encoder. Enforce tenant_id, status, location, compensation, freshness, and authorization filters in the index or service before evidence reaches the LLM—not by redacting after generation.

Write actions—submitting a résumé, sending a message, or editing a JD—need typed schemas, server-side reauthorization, idempotency keys, preview and confirmation, timeout and compensation behavior, and audit logs. The model may propose an action; it must not bypass business permissions.

Step 8: train matching as a separate system

Large-scale job matching remains a retrieval and recommendation problem.

Use a two-tower model to encode candidate and job representations for ANN retrieval. Apply a cross-encoder or learning-to-rank model to the retrieved set, then rerank for freshness, diversity, deduplication, and business constraints.

candidate profile → candidate tower ─┐
                                     ├─ similarity → ANN Top-K
job profile       → job tower ───────┘
                                           ↓
                            cross-encoder / learning-to-rank
                                           ↓
                         rules, freshness, diversity, fairness
                                           ↓
                              LLM-generated explanation

An LLM can enrich features, produce weak labels, and explain results. It should not iterate over millions of jobs at inference time.

Labels also need care. A click is not ground truth. Exposure is controlled by the previous model; clicks respond to titles and compensation; conversations depend on recruiter activity. Use layered events—impression, click, save, apply, reply, interview, hire—and account for position bias, negative sampling, and delayed outcomes.

Two-tower retrieval can use a contrastive objective. For candidate vector u, positive job v+, and jobs in the batch:

L_retrieval = -log exp(sim(u,v+)/τ) / Σ_j exp(sim(u,v_j)/τ)

Random negatives are often too easy. Add hard negatives such as the same title at a different seniority or the same skill set in an incompatible location. An exposed but unclicked job is not necessarily negative—it may have appeared too low—so preserve exposure position and filter likely false negatives.

The ranker can combine semantic crosses, structured compatibility, freshness, and behavior. LLM explanations must read the factual features and source evidence used by the final ranking, avoiding one mechanism for ranking and an invented story for explanation.

Step 9: build evaluation before large training runs

Without a private recruitment benchmark, the team cannot know whether domain training helped.

Understanding and generation

CapabilityMetrics
Field extractionPrecision, recall, F1, schema-valid rate
JD rewritingFactual fidelity, completeness, violations, acceptance
Fit explanationEvidence precision, omissions, unsupported-claim rate
RAGRecall@K, citation correctness, groundedness, abstention
Tool useArgument accuracy, execution success, unauthorized calls, duplicates
SafetyPII leakage, discriminatory language, prompt-injection success

Matching and ranking

  • Recall@K for retrieval;
  • MRR and NDCG@K for ordering;
  • coverage for long-tail jobs and new candidates;
  • calibration between scores and outcomes;
  • subgroup differences across legitimate evaluation slices;
  • online valid-contact, reply, application, interview, and complaint rates.

Freeze the test set by time and exclude future feedback from training. General leaderboards cannot replace domain evaluation.

Release through shadow traffic first: generate outcomes without affecting users, compare against production, then progress through a small canary only after safety, quality, latency, and cost gates pass.

Step 10: production is more than starting vLLM

Open-source engines such as vLLM and SGLang provide efficient serving, but production also requires gateways, routing, caching, degradation, and observability.vLLM

API gateway
   ↓
identity, quota, length, and risk checks
   ↓
task router
   ├─ small model: classification, extraction, rewriting
   ├─ larger model: complex analysis and dialogue
   ├─ embedding/reranker: retrieval and matching
   └─ rule service: constraints and safety
   ↓
batching, KV cache, timeout, circuit breaker
   ↓
schema, citation, and safety validation
   ↓
logs, traces, cost, and quality sampling

Observe time to first token, time per output token, total latency, queue time, token counts, GPU utilization, cost per task, timeout, cancellation, retry, fallback, JSON failures, tool failures, and unsupported claims—split by model, task, language, and version.

A model upgrade is not one file replacement. Prompts, tokenizers, retrieval, tool schemas, quantization, and decoding parameters all affect behavior and need coordinated versioning.

A first capacity approximation is:

replicas ≈ peak QPS × P95 service time
           ÷ safe concurrency per replica × headroom

Correct it with load tests using real input and output lengths. Benchmark short extraction, long-résumé analysis, and multi-turn dialogue separately; averages hide long-tail out-of-memory failures. Compare BF16, FP8/INT8, and lower-bit quantization on the recruitment gold set before production—throughput alone is not an acceptance test.

The safety line in recruitment

Recruitment is not ordinary text generation. A bad recommendation wastes time; an opaque rejection can affect a person's livelihood.

China's Personal Information Protection Law requires transparency and fairness when personal information is used for automated decision-making.Personal Information Protection Law

At minimum:

  1. Do not use sex, age, ethnicity, family status, or health as matching features without a lawful and justified basis;
  2. test proxy effects from school, address, career gaps, and similar attributes;
  3. never let the LLM make final hiring or rejection decisions alone;
  4. ground important recommendations in job and résumé evidence;
  5. provide human review, correction, opt-out, and appeal paths;
  6. enforce résumé authorization at retrieval, prompt, log, and cache layers;
  7. audit dataset, model, prompt, and tool-call versions;
  8. complete applicable security assessment, algorithm filing, or model registration before public launch.

Removing a name does not make a model fair. Schools, postal codes, employment years, and writing style can become identity proxies. Fairness review requires lawful subgroup analysis and joint ownership across legal, ethics, recruitment, product, and ML teams.

An executable 12-week plan

PeriodGoalDeliverable
Weeks 1–2Tasks and baselinesThree scenarios, gold evaluation set, production baseline, compliance checklist
Weeks 3–4Data and ontologyOntology v1, lineage, de-identification pipeline, dataset v1
Weeks 5–6Open-model PoCThree-model benchmark, LoRA SFT, error taxonomy
Weeks 7–8RAG and matchingHybrid retrieval, reranker, two-tower baseline, citations
Weeks 9–10Preference and safetyDPO set, safety suite, red team, authorization and audit
Week 11Shadow and canaryShadow report, latency/cost report, 1–5% canary
Week 12ReviewBusiness impact, regression report, decision on scale and model size

Define release gates in advance: extraction F1, schema validity, unsupported-claim rate, zero severe PII or authorization failures, P95 latency and cost budgets, and a statistically credible lift in acceptance or valid conversations.

Exact thresholds must come from the business baseline, not another company's blog post.

Seven common failure modes

  1. Train first, search for a use case later: the result is a demo chatbot without a business outcome.
  2. Treat database access as training consent: résumé, conversation, and behavior purposes are ignored.
  3. Randomly split rows: users and templates leak into test data and inflate scores.
  4. Build only an LLM: no scalable retrieval, high latency, and high cost.
  5. Trust general benchmarks: the model solves math but cannot extract salary evidence consistently.
  6. Treat clicks as truth: the new system learns the old system's exposure and position bias.
  7. Automatically reject candidates: no evidence, explanation, human review, or fairness control.

My conclusion

The moat in a recruitment model is not renaming an open model or maximizing parameter count.

It comes from:

  • a consistent, evolving job and skill ontology;
  • lawful governance of high-quality recruitment data;
  • correct boundaries between search, recommendation, LLMs, RAG, and rules;
  • evaluation against both business outcomes and safety constraints;
  • a loop that turns failures, edits, and final outcomes back into better data.

Without these five elements, a self-developed model is an expensive chatbot.

With them, even a 7B–14B open-weight model can create real value in version one.

Building a recruitment LLM from zero to one is not writing a Transformer from zero. It is building, from zero, a system that connects recruitment knowledge, business data, model capability, and real outcomes.

References

30 / 41

The Moat in Large Language Models Is Not Just Code—It Is Data

As Transformer implementations, training frameworks, and open weights become widely available, the scarce advantage shifts toward data quality, data mixtures, verification, governance, and the feedback loop that improves them.
Aug 10, 2026 · 14 min read

Large language model code is no longer as mysterious as it was a few years ago.

Transformers are public. Attention is public. Mature open-source implementations now exist for pretraining, supervised fine-tuning, preference optimization, quantization, and serving. PyTorch, Hugging Face, DeepSpeed, Megatron-LM, TRL, and vLLM have turned many capabilities that once belonged only to large research labs into reusable engineering components.

For an experienced team, building a model that can train, fine-tune, and serve is genuinely easier than before.

But making a model run is not the same as making it useful.

My view has become increasingly clear:

As model architectures converge and training code becomes widely available, the real competition is shifting from “Can we implement the model?” to “Do we have the right data, how good is it, and can we continuously improve it?”

This does not make algorithms, compute, or systems engineering unimportant. Frontier training still demands sophisticated distributed systems, optimization, reliability, evaluation, and enormous compute. The difference is that baseline code is increasingly accessible, while valuable data and the system that produces it remain difficult to copy.

Parameters determine capacity; data determines what is learned

Machine learning has a simple and durable rule:

Garbage in, garbage out.

A language model does not first decide whether a document is true and then choose whether to learn from it. Pretraining still centers on predicting tokens from context. If a pattern appears often enough in the training data, the model may encode it in its parameters.

Fill the corpus with SEO-generated pages, and the model learns fluent emptiness. Fill a code corpus with duplicated repositories, obsolete dependencies, and incorrect implementations, and the model may produce unsafe code with greater confidence. If medical, legal, or financial material is wrong, adding parameters does not automatically turn it into truth.

“Data determines the ceiling” is directionally right, but it can be stated more precisely:

Parameter count determines how much pattern capacity a model has. Data determines which patterns it encounters. Algorithms and optimization determine how effectively those patterns are absorbed. Evaluation determines whether the team actually knows what the model learned.

Capability is not controlled by one variable. Architecture, compute, tokenization, objectives, data, post-training, and inference strategy interact. Data matters because it defines the boundary of experience available to the model.

Data quality is more than removing dirty text

Data quality is often reduced to deleting corrupted text, advertisements, explicit content, and duplicates. That is only the first layer.

For a language model, quality has at least five dimensions:

DimensionQuestion
CorrectnessAre facts reliable? Does the code compile and pass tests?
DiversityDoes the corpus cover different languages, domains, tasks, views, and styles?
RepresentativenessDoes its distribution resemble the situations the model will face?
Safety and complianceDoes it contain personal data, secrets, harmful material, or licensing risk?
LearnabilityIs the content complete, clear, structured, and information-dense?

A cleanly formatted dataset without offensive language is not necessarily high quality. If every example repeats the same view, the resulting model will still be narrow and biased.

Quality and quantity are not simple opposites. General models need scale for coverage. Quality determines whether those tokens deserve the compute spent on them.

Do not invent percentages that were never disclosed

The precise composition of a training corpus is often among a model developer's most valuable secrets.

For Llama 3, Meta disclosed that the pretraining corpus contained more than 15 trillion tokens from publicly available sources. It was roughly seven times larger than Llama 2's corpus, contained four times as much code, and included more than 5% high-quality non-English data across over 30 languages. Meta also described heuristic filters, NSFW filters, semantic deduplication, quality classifiers, and experiments used to choose the final mixture.Meta's official Llama 3 announcement

Meta did not publish a complete mixture such as “50% Common Crawl, 17% GitHub, and 10% books and ArXiv.”

The rigorous claim is that large-model corpora commonly contain web pages, code, books, papers, encyclopedic material, conversations, and multilingual text. Their exact proportions depend on the target capabilities and should not be presented as official figures without a source.

That distinction reveals something important:

Architecture can be described in a paper. The data recipe often remains the commercial secret.

Phi-1 demonstrated data efficiency, not universal small-model superiority

Microsoft's Phi-1 is frequently cited as proof that data quality matters, but its conclusion is often overstated.

Phi-1 is a 1.3-billion-parameter model trained on approximately 6 billion tokens of filtered code data plus about 1 billion tokens of synthetic textbooks and exercises. It achieved strong results on coding benchmarks including HumanEval and MBPP.“Textbooks Are All You Need”

Its strongest lesson is:

In a focused domain with verifiable outcomes, information-dense data and a good teaching progression can dramatically improve training efficiency.

The work did not establish that a 1.3B model universally beats 7B models. Coding, world knowledge, multilingual capability, long-context behavior, and writing are different dimensions. One coding benchmark cannot stand in for all of them.

Data quality is not magic. It does not remove the boundary of the task or create knowledge absent from the corpus.

LIMA demonstrated “less but better,” not “1,000 equals 52,000”

LIMA fine-tuned an already pretrained 65B LLaMA model using only 1,000 carefully selected prompts and responses, and obtained strong instruction-following behavior.The LIMA paper

The precondition matters: the base model had already acquired substantial knowledge during pretraining. Supervised fine-tuning was largely teaching it how to organize, invoke, and express that capability.

The more defensible lesson is:

For a capable pretrained model, a small, carefully selected, well-covered instruction dataset can outperform a much larger collection of noisy and repetitive instructions.

It does not mean that any 1,000 examples equal 52,000 Alpaca examples, or that every enterprise needs only 1,000 labels. Base-model capability, task boundaries, coverage, and evaluation criteria all change the outcome.

What a production data pipeline should look like

Production data engineering is not a one-off cleaning script. It is a traceable, reproducible, measurable, and continuously evolving manufacturing system.

source registration and license review
        ↓
collection, parsing, and normalization
        ↓
main-content extraction and structure recovery
        ↓
language, domain, and content-type detection
        ↓
rule-based filtering and anomaly detection
        ↓
quality scoring and stratified sampling
        ↓
exact, fuzzy, and cross-source deduplication
        ↓
PII, secret, safety, and compliance processing
        ↓
train–evaluation contamination detection
        ↓
mixture design, curriculum, and token budgeting
        ↓
small-scale ablation experiments
        ↓
capability, safety, bias, and regression evaluation
        ↓
versioned release

The ordering is not universally fixed. Exact deduplication can move earlier to reduce downstream cost. Quality-aware deduplication may score first and retain the best document from each duplicate cluster. The best practice is not memorizing an order; it is giving every stage defined inputs, outputs, metrics, and lineage.

1. Govern the source before cleaning it

Every data batch should record its origin, acquisition date, license, usage restrictions, language, domain, and processing history.

Without provenance, a team cannot respond to copyright concerns, deletion requests, contamination, or reproducibility failures. What an enterprise needs is not merely object storage full of files, but a data catalog and lineage system.

2. Parsing quality constrains everything downstream

Menus, advertisements, recommendation links, cookie notices, and comments can overwhelm the main text of a webpage. PDFs introduce repeated headers, broken reading order, misplaced tables, and OCR errors.

If parsing destroys semantic structure, later stages are only cleaning characters in the wrong order. Data engineering begins by recovering document boundaries and structure as faithfully as possible.

3. Language identification needs more than fastText

fastText is a useful baseline, but short passages, dialects, low-resource languages, and code mixed with natural language are easy to misclassify.

Production pipelines usually combine model predictions, character-level rules, source metadata, and human sampling. Thresholds should be calibrated per language. Applying English-centric quality rules to every language often deletes the rarest multilingual data systematically.

4. Perplexity is not a synonym for quality

KenLM perplexity, heuristics, and learned classifiers are all useful, but no single score represents quality.

High perplexity may indicate corrupted text. It may also indicate specialized terminology, new knowledge, or a low-resource language. Aggressive global thresholds can remove the material that is hardest to replace.

A stronger approach combines multiple signals, buckets data by language and domain, performs human audits, and validates filtering choices through training experiments.

5. Deduplication is more than MinHash

One article may be syndicated dozens of times. A GitHub repository may have thousands of forks. A tutorial may be copied with only its title and a few sentences changed.

A complete strategy can include:

  • URL and content-hash deduplication;
  • exact document-level deduplication;
  • fuzzy deduplication using MinHash, LSH, or embeddings;
  • cross-source and cross-language deduplication;
  • contamination checks between training and evaluation sets.

The goal is not to delete the largest possible amount. It is to keep the most complete, trustworthy, and highest-quality member of each duplicate cluster.

6. PII and safety require more than regular expressions

Email addresses, phone numbers, identity numbers, home addresses, API keys, and internal code can all enter a corpus.

Regular expressions handle stable formats, but production systems also need entity recognition, secret scanners, classifiers, and human review. They need deletion workflows too: when data must be withdrawn, the team should know which dataset and model versions consumed it.

Harmful content should not simply be removed by keyword. A model needs some understanding of attacks, fraud, and dangerous material in order to detect and refuse them. Context, proportion, labeling, and the training objective matter more than mere occurrence.

A data mixture is a capability budget

Training compute is finite. Increasing the sampling rate of one source reduces the training opportunity available to another.

Code contains structure, long-range dependencies, and partially verifiable outcomes. More code may improve programming and some reasoning tasks, but it does not guarantee stronger general reasoning. Too much can crowd out natural language, domain knowledge, and multilingual ability.

More Chinese data generally improves Chinese understanding, expression, and knowledge coverage. The number of tokens required to encode Chinese, however, is governed mainly by the tokenizer's vocabulary and segmentation strategy. Corpus share and tokenization efficiency are related, but they are not the same variable.

Books and papers can provide dense knowledge and long-form structure. Social media can cover current language and public opinion while introducing misinformation, abuse, and demographic bias.

There is no universal golden ratio.

A data mixture is not a shopping list of ingredients. It is the product strategy projected into the training corpus.

A coding model, a Chinese legal model, and a general assistant should not share the same recipe.

The core of synthetic data is verification, not generation

As high-quality natural data becomes scarcer, synthetic data becomes increasingly useful. Strong models can generate textbooks, reasoning problems, code, question-answer pairs, and preference examples while controlling difficulty and covering rare tasks.

But synthetic data does not create trustworthy knowledge from nothing.

Teacher errors propagate to students. Repetitive templates reduce diversity. Repeatedly generating and filtering with the same model can amplify its existing biases.

The decisive capability is therefore verification:

  • check mathematics against answers and constraints;
  • compile code and run tests and static analysis;
  • execute tool calls and compare real outcomes;
  • ground factual questions in retrieved sources;
  • use multi-model review and human sampling where automatic verification is impossible.

Without a verifier, synthetic data is simply a cheaper and faster way to manufacture uncertainty.

Data engineering must close the loop with evaluation

After a cleaning run, the key question is not “How many tokens remain?” It is: which capabilities improved, which regressed, and why?

model failures and user feedback
        ↓
failure taxonomy and root-cause analysis
        ↓
identify gaps, bad samples, or mixture problems
        ↓
add, correct, resample, or relabel data
        ↓
small training runs and ablations
        ↓
offline evaluation, red teaming, and online observation
        ↓
promote into the next dataset version after quality gates

At least four kinds of metrics are required:

  1. Data metrics: duplication, language distribution, domain coverage, quality scores, and PII detections;
  2. Training metrics: loss by data bucket, gradient anomalies, token utilization, and convergence;
  3. Capability metrics: target-task accuracy, code pass rate, factuality, and instruction following;
  4. Risk metrics: hallucination, harmful output, privacy leakage, bias, and benchmark contamination.

Without versioned data, training cannot be reproduced. Without ablations, a team cannot prove that a data batch helped. Without production feedback, offline leaderboards cannot establish business value.

What the data moat means for an enterprise

Most companies should not pretrain a general foundation model from scratch. The better investment is converting internal knowledge, workflows, and expert feedback into governed and measurable data assets.

I would prioritize the following:

  1. Select three valuable business tasks with measurable outcomes;
  2. build a separate evaluation set for each task before collecting “all the data”;
  3. inventory sources, permissions, licenses, sensitivity, and ownership;
  4. create pipelines for cleaning, deduplication, redaction, versioning, and lineage;
  5. validate value through RAG or small fine-tuning runs before scaling training;
  6. continuously feed back corrections, reviews, and failure cases;
  7. use business outcomes—not token counts—to decide where to invest.

The most defensible enterprise data is rarely another copy of the public web. It is authorized workflow data, expert judgment, customer feedback, failure cases, and outcome labels.

Such datasets may be small, but they are closely tied to real results and difficult for competitors to reproduce.

My conclusion

Algorithms and code still matter. Training systems, architecture, optimizers, compute, and inference engineering do not lose their value merely because open source exists.

But as the baseline stack becomes shared infrastructure, the advantages that remain hard to copy are:

  • long-lived, authorized domain data;
  • an understanding of provenance, bias, and quality;
  • expert judgment and feedback;
  • the ability to turn model failures into better training data;
  • evaluation tied to real business outcomes.

“Garbage in, garbage out” is only the first conclusion.

I would state the complete version this way:

Parameters determine capacity. Compute determines training scale. Algorithms determine learning efficiency. Data determines what the model ultimately learns.

As code becomes common infrastructure, the real competition is no longer about who can make a model run. It is about who can continuously feed it correct, scarce, lawful, and verifiable data—and prove that the model becomes better as a result.

That is the real data moat in the age of large language models.

References

31 / 41

Agent Architecture for SMBs: A 90-Day Path to Production

Do not copy an enterprise AI platform. Build a production agent with deterministic orchestration, narrow tools, least privilege, evaluation, and measurable business outcomes.
Aug 07, 2026 · 16 min read

An enterprise agent should not be designed as “a chatbot plus a few APIs.” But a small or midsize business should not copy a global company's central agent platform, marketplace, and knowledge infrastructure either. The former can answer but cannot execute reliably; the latter exhausts budget before it creates business value.

An SMB needs a minimum production architecture: an agent that completes one bounded business loop—understanding an objective, acquiring context, using tools, verifying the result, and returning control to a person when uncertainty or risk is too high.

This article answers four practical questions: which use cases deserve an agent, how to structure the production system, how to deploy it in 90 days, and how to measure the outcome honestly.

Does the problem need an agent?

Task shapeDefault solution
Fixed rules, structured input, known pathConventional code, RPA, or workflow engine
Generation, summarization, classification, extractionOne LLM call with structured output
Known sequence with a few semantic decisionsLLM workflow
Unknown path requiring tool selection and adaptationAgent
Independent specialization, real parallelism, or permission isolationA small multi-agent system

OpenAI describes an agent's foundation as model, tools, and instructions, and recommends use cases involving complex judgment, unmanageable rules, or substantial unstructured data. Anthropic reaches a similar conclusion from production work: begin with simple, composable patterns and add autonomy only when its value covers the extra latency, cost, and failure surface.

If a few deterministic branches solve the task reliably, do not build an agent yet.

Adapt enterprise layers to SMB constraints

Gateway, orchestration, tools, knowledge, policy, and evaluation remain useful ideas from enterprise architectures. The difference is that an SMB should combine them and split only under proven load.

Enterprise patternSMB default
Central platform and agent marketplaceOne task service with scenario-specific skills
Many domain agentsOne agent first; split only with evidence
Enterprise knowledge platformGovern authoritative data for the current workflow
General tool hubStart with three to five narrow tools
Broad multi-model routingSmall model for extraction; strong model for key judgment
Dedicated governance organizationPolicy as code, a business owner, and explicit approvers

An SMB may have only two to five people operating the entire system. Its architecture must remain simple, replaceable, observable, reversible, and economically tied to accepted results.

The minimum production architecture

Production agent architecture for an SMB

The architecture separates model judgment from real execution. An LLM may propose a plan or tool call; a deterministic policy engine decides whether it is allowed. Tools expose narrow business operations—not database administration or an unrestricted shell.

1. Interaction and gateway

Requests can arrive through a web app, messaging platform, email, CRM button, API, or event. The gateway handles identity, tenancy, rate limits, and normalization into a task envelope containing task type, actor, input, risk, budget, and acceptance criteria. Authorization, cost, and evaluation then belong to a business action rather than an untraceable conversation.

2. Deterministic orchestration first

Code controls known flows: load a lead, enrich it, score it, draft a response, request approval, and update CRM. Let the agent choose a tool only when the next step genuinely depends on its findings.

Production states include queued, running, waiting_approval, succeeded, failed, and cancelled. Every step needs a timeout, retry ceiling, idempotency key, and checkpoint. Long work belongs on a queue, not inside one open HTTP request.

3. Agent runtime and model gateway

Agent = Instructions + Tools + Task State + Policies + Evaluation

Treat models as replaceable dependencies. Small models classify, extract, and route; stronger models plan and synthesize. Set token, tool-call, time, and cost ceilings per task. Validate output against a schema before it becomes an action. Failed validation receives a bounded repair attempt or human escalation.

Keep vendor SDK calls out of business logic. Model and prompt changes pass regression evaluations before gradual rollout.

4. Separate state, facts, knowledge, and preferences

  • Task state: progress, tool results, and unresolved questions.
  • Business facts: CRM, ERP, and order systems remain sources of truth.
  • Knowledge: manuals, policies, and templates are retrieved with citations and access control.
  • Long-term preferences: store only with a purpose, consent, expiry, and deletion path.

Webpages, emails, and documents are untrusted inputs. Their instructions cannot become system commands. Retrieval must enforce tenant isolation, data classification, source, version, and permission filters.

5. Narrow tools and least privilege

Avoid a universal “operate CRM” tool. Prefer get_customer, list_recent_orders, draft_followup, and update_lead_status, with typed parameters and business-rule validation.

LevelExampleDefault control
L0 readSearch products or approved customer factsExecute and log
L1 reversible writeCreate a draft or internal labelAutomatic or policy-approved
L2 external impactSend email, change quote, update customer stateHuman approval before execution
L3 high impactPay, delete, sign, or deployTwo-person approval or prohibit

The policy engine—not the model—validates identity, resource scope, data class, approval state, and financial thresholds. OWASP similarly recommends least-privilege tools, explicit authorization for sensitive actions, distrust of external input, and separation of reasoning from irreversible execution.

6. Evaluation and observability

Record task, prompt or skill, and model versions; cited context; summarized tool parameters; approvals; latency; tokens; cost; final state; and human corrections. Redact credentials and personal data.

Evaluate three levels:

  1. Step: schema validity, field completeness, and tool choice.
  2. Trajectory: unnecessary loops, privilege attempts, waste, and recovery behavior.
  3. Outcome: acceptance, rework, cycle time, and cost per accepted result.

Start with 30–50 historical tasks covering ordinary, edge, and adversarial cases. Rerun them after changes to models, prompts, tools, knowledge, or permissions. NIST's Govern–Map–Measure–Manage structure provides a useful lightweight governance backbone.

Default to one agent

Begin with one orchestrator, one agent, and several narrow tools. Split only when evidence shows that unrelated contexts interfere, genuine parallelism saves meaningful time, an independent evaluator must not share the generator's path, or roles require different permissions or models.

Even then, use central orchestration and structured messages. Cap turns, total budget, delegation depth, and execution time. Agents holding a long “meeting” is not intelligence; it is uncontrolled cost.

First workflow: lead research and follow-up

Consider a 30–80 person B2B services company. Sales repeatedly inspects inbound forms, researches companies, assesses fit, drafts outreach, and updates CRM. The task is frequent, time-consuming, reviewable, and safe to begin in draft-only mode.

New lead event
  → validate and deduplicate
  → read CRM and approved sources
  → create an evidence-backed profile and score
  → validate fields, citations, and prohibited claims
  → draft follow-up and next action
  → sales approval
  → update CRM and turn human corrections into eval cases

Version one receives read and draft permissions only. After two stable weeks above the acceptance threshold, it may update reversible internal fields. External communication continues to require human confirmation.

A 90-day rollout

Weeks 1–2: scenario and baseline

  • Select one workflow and one accountable business owner.
  • Sample 50 historical tasks and measure handling time, waiting, first-pass acceptance, rework, and exceptions.
  • Define forbidden outcomes: cross-tenant access, unsupported claims, and unapproved external action.
  • Map sources of truth, data classes, and tool permissions.

Deliverables: use-case contract, baseline, 50-case evaluation set, risk register, and stop criteria.

Weeks 3–4: read-only MVP

  • Implement the task envelope, state machine, model gateway, and three to five read tools.
  • Produce structured recommendations and drafts without writing to business systems.
  • Add traces, cost accounting, and error categories.
  • Shadow-run with two or three real users.

Release gate: no authorization failure, at least 95% critical-field completeness, 100% traceable citations, and cost within budget.

Weeks 5–8: bounded writes and approval

  • Add reversible writes, idempotency, and approval records.
  • Convert human edits into failure categories and regression cases.
  • Test prompt injection, sensitive-data leakage, excessive tool access, and unbounded loops.
  • Add timeouts, circuit breakers, fallback, and a kill switch.

Release gate: agreed first-pass acceptance, stable P95 latency and accepted-result cost, and zero critical security failures.

Weeks 9–12: controlled trial

  • Compare old and new processes for at least two full business cycles.
  • Include model, infrastructure, review, maintenance, and failure costs.
  • Reuse stable gateway, approval, evaluation, and connector components.
  • Reduce scope or stop if the exit criteria are not met.

Deliverables: outcome dashboard, incident playbook, runbook, and evidence-based expand/stop decision.

Report outcomes honestly

Do not claim an “80% efficiency gain” before the pilot. Freeze the baseline first and fill in measured results afterward.

MetricBaselineAcceptance thresholdMeasured result
End-to-end cycle timeMeasure≥ 40% lowerFill after pilot
Human handling timeMeasure≥ 30% lowerFill after pilot
First-pass acceptanceMeasure≥ 85%Fill after pilot
Critical-fact citationMeasure100%Fill after pilot
Human escalationN/AExplainable and trending downFill after pilot
Cost per accepted resultMeasureBelow human baselineFill after pilot
Critical privilege/data incident00Fill after pilot
monthly net value
= hours saved × fully loaded labor cost
+ incremental business value
- model and infrastructure cost
- review and maintenance cost
- expected loss from failure

The useful unit is cost per accepted business result, not cost per model call.

Buy, assemble, or build?

  1. Buy an agent embedded in existing SaaS when its permissions and audit are sufficient.
  2. Assemble a managed model, workflow or agent SDK, and an owned policy layer when cross-system execution matters.
  3. Build a general platform only when scale, regulation, or durable differentiation can fund the long-term team.

Whichever route you choose, own the task protocol, tool contracts, evaluation set, and audit data. These assets will outlast a particular framework.

Production checklist

  • A business owner, system owner, and kill-switch owner are named.
  • Every task has acceptance criteria, budget, timeout, and step limit.
  • Business facts remain in a system of record.
  • Tools are least-privilege; no unrestricted write or code execution exists.
  • External, financial, destructive, and production actions require approval.
  • Tenant, user, session, and long-term memory are isolated.
  • Logs are redacted and runs are reconstructable from task IDs.
  • Offline evals, shadow mode, gradual rollout, and rollback exist.
  • Model, retrieval, and tool failures have degraded paths.
  • Monthly review decides whether to expand, narrow, or retire the workflow.

Conclusion

An SMB does not win by building a more complicated platform than a large enterprise. It wins with a shorter loop from problem discovery to pilot, measurement, and correction.

Use deterministic workflows as the skeleton and an agent only for uncertainty that requires judgment. Grant read and recommendation access before execution authority. Establish evaluation and audit before pursuing autonomy and scale.

One agent that reliably completes a business loop, knows when to stop, and produces evidence is worth more than a department of “digital employees” no one can evaluate.

Further reading

32 / 41

Building Docket with Pragmatic DDD

A code-based look at how Docket uses bounded contexts, domain services, and a modular monolith to manage a professional-services workflow—and how the architecture can evolve.
Aug 07, 2026 · 12 min read

Docket is a workspace for lawyers and professional-service teams. It connects lead acquisition, document collection, agreements, delivery, payments, reviews, and referrals. The hard part is not CRUD. It is keeping rules consistent across roles, stages, and side effects while the product continues to change.

I built it around the core ideas of domain-driven design without reproducing a textbook directory structure. The precise description is a modular monolith organized around bounded contexts: one deployable system and local transactions, with business boundaries used to contain complexity.

Docket's pragmatic DDD architecture

Start with business boundaries

Page-based decomposition produces “project page” and “admin page” modules. Table-based decomposition produces a collection of CRUD services. Neither tells us where a business capability begins and ends.

Docket instead separates Identity, Lead, Project, Collection, Agreement, Delivery, Finance/Billing, Profile/Portal, and Notification. Each context owns a distinct language and reason to change. Agreement owns signing rules; Collection owns upload and review; Project owns the collaboration lifecycle. Notification supports them but does not make their decisions.

Docket bounded-context map

ContextResponsibilityRepresentative objects
IdentityAccounts, authentication, plan identityLawyer, LoginLog
LeadLeads, follow-ups, conversionLeadEntry, LeadFollowUp
ProjectProject lifecycle and collaborationProject, ProjectItem
CollectionUpload, review, acceptanceDocumentFile, ReviewService
AgreementMulti-party signingProjectAgreement, AgreementSigner
DeliveryDeliverables and fulfillmentDelivery, DeliveryPhoto
Finance / BillingFees, payments, plans, usageFeeRecord, PaymentEntry, CoinAccount
Profile / PortalAcquisition pages and client accessLawyerProfile, PortalProjectService

Three responsibilities in the codebase

Flask's application factory composes three visible layers.

app/api/*_api.py contains Blueprints and owns HTTP concerns: authentication, input parsing, service invocation, and responses. It should not decide when a project is complete.

app/{domain}/services.py implements use cases and business rules. Collection handles validation, batches, and review; Agreement coordinates invitations, rejection, completion, and result PDFs; Portal composes client-facing views. Today these services combine application orchestration with parts of the domain layer.

app/{domain}/models.py contains SQLAlchemy entities, relationships, and domain-flavored enums. MySQL, email, files, scheduled tasks, AI providers, and blockchain notarization supply infrastructure capabilities.

Next.js
  → Flask Blueprint: protocol and authorization
  → Domain Service: use case and rules
  → ORM / db.session: state and transaction
  → Notification / File / Scheduler: side effects
  → JSON response

The useful constraint is directional: pages do not understand storage, APIs do not duplicate business rules, and domain modules do not depend on presentation details.

Aggregates are consistency boundaries

Project is the clearest aggregate entry point. External clients address it with public_id; integer keys remain internal for joins. ProjectItem and DocumentFile change through explicit use cases under ownership, authorization, and state constraints.

Anonymous flows use separate capability tokens. A collection client_token and an agreement signer_token grant access as well as identify a resource, while a public_id still requires JWT authorization. Keeping those concepts separate is a domain security rule, not cosmetic URL design.

Coordinating contexts without premature distribution

A customer journey crosses contexts: a lead becomes a project, the client uploads documents, a lawyer reviews them, parties sign, work is delivered, and a review may create a referral.

Docket currently coordinates this through explicit service calls and same-database transactions. That keeps deployment and debugging direct. Email, PDF generation, scheduled work, and notarization are separated as supporting services or queued tasks where their failure should not obscure the primary decision.

The important rule is ownership. Agreement determines whether signing is complete. Project may react to that fact, but should not reproduce the signing algorithm. Contexts exchange identifiers and outcomes, not fragments of duplicated rules.

Why bounded contexts do not imply microservices

All Docket contexts currently share a Flask process and MySQL database. Local transactions are valuable across project, collection, and notification work, while distributed calls, message consistency, and operational infrastructure would add cost before they add leverage.

A modular monolith only works when the boundaries are real: independent directories, clear entry points, and explainable data ownership. Service extraction should follow evidence such as independent scaling, fault isolation, team autonomy, or regulatory isolation—not the number of modules on a diagram.

This is not “pure DDD”

The implementation deserves an honest label. Services directly use SQLAlchemy models and db.session; persistence models also act as domain entities; some services serialize responses and query across contexts. Repositories, ports, and domain events are not universal abstractions.

That is a deliberate trade-off during rapid discovery. Clear language and module boundaries are more valuable than speculative interfaces. As complexity justifies it, the design can evolve incrementally:

  1. Move critical state machines and invariants from large services into entities or domain policies.
  2. Define ports for file storage, email, AI, and notarization, with infrastructure adapters behind them.
  3. Introduce application DTOs so API representation no longer leaks into domain services.
  4. Publish domain events for agreement completion, project acceptance, and lead conversion; use an outbox for reliable delivery.
  5. Enforce module dependency rules and prohibit direct writes into another context's data.
  6. Extract a service only when its runtime boundary is genuinely different.

How to test whether the boundaries work

The diagram is not the acceptance criterion; change cost is. A useful boundary passes three tests: a rule change stays mostly inside one module, a business action has one obvious entry point, and a failure can be attributed to a context, use case, and side effect.

Tests should mirror those boundaries: domain tests for invariants and transitions, service tests for use cases and transactions, API tests for authentication and contracts, and a small set of end-to-end tests for the journey from acquisition to delivery.

The lasting lesson

DDD is valuable when code structure follows business structure. For Docket, the decisive questions are not whether folders are named domain, application, and infrastructure. They are: who owns project state, who accepts a document, who completes an agreement, who owns delivery, and how facts cross those boundaries.

Docket establishes those boundaries in a modular monolith first, then lets architectural evidence drive further separation. The goal is not ceremonial purity. It is to ensure that every increase in complexity still has a clear, testable, and evolvable home in the business model.

Project

33 / 41

Xinxu Cloud Brain AI: Building a Reliable Multimodal Emotion System

A production architecture for voluntary text, speech, and drawing inputs, combining modality-specific encoders, robust fusion, multi-task learning, calibrated uncertainty, privacy governance, and continuous evaluation.
Aug 07, 2026 · 17 min read

Xinxu Cloud Brain AI is designed to understand emotional cues in text, speech, and drawings that users voluntarily provide, enabling more empathetic human-computer interaction.

The challenge is not feeding three inputs into one large model. Each modality contains different evidence; emotions can coexist and change with context; and a model output can only express a hypothesis from available evidence. It cannot replace self-report or become a psychological diagnosis.

The system therefore follows three principles: preserve modality-specific evidence, model uncertainty explicitly, and leave final interpretation with the user.

Xinxu multimodal emotion architecture

Define the task before choosing a model

Happy, sad, angry, fearful, surprised, disgusted, and neutral can remain useful presentation labels, but a single seven-way Softmax is too restrictive for training. Expressions can contain sadness and anger together, while “neutral” may simply mean insufficient evidence.

A useful output combines multi-label probabilities, continuous valence and arousal, calibrated uncertainty, modality-specific evidence, and a policy decision:

{
  "emotions": [
    {"label": "sadness", "probability": 0.68},
    {"label": "anxiety", "probability": 0.31}
  ],
  "valence": -0.62,
  "arousal": 0.48,
  "uncertainty": 0.27,
  "decision": "ask_for_confirmation"
}

Intensity must come from a trained and calibrated regression head, not an arbitrary number generated by a language model.

Preserve three kinds of evidence

The text encoder handles semantics, negation, irony, context, and conversation history. ASR transcripts enter this branch with timestamps, confidence, and language metadata, clearly separated from user-written text.

Speech must not be reduced to a transcript. Pitch, energy, rate, pauses, voice quality, and rhythm carry information that words discard. A speech encoder should process waveform or acoustic representations alongside ASR semantics. Audio normalization, voice activity detection, segmentation, and quality scoring belong in preprocessing; low-quality audio should reduce modality weight.

Drawings are highly personal. Color, line, and composition can provide context, but there is no universal mapping such as black meaning sadness. The vision branch models content and structure. Stroke trajectory and drawing duration should be used only with explicit consent. Augmentations must preserve task semantics: strong color jitter and cropping can change or delete the very cue being modeled. Synthetic images may support pretraining or rare-pattern augmentation, but must be provenance-tagged and excluded from validation and test sets.

Why one VLM is not the entire architecture

A vision-language model can jointly understand images and text, but an ordinary VLM does not consume raw audio and may not learn emotion boundaries for the target population and culture. Converting speech only to text discards acoustic evidence.

Xinxu uses modality-specific text, speech, and vision encoders. Adapters map their outputs to a shared space, followed by gated fusion or a cross-modal transformer. A VLM can act as a visual-semantic teacher, weak labeler, or explanation generator; supervised heads own classification, dimensional regression, and uncertainty.

This design also works when a user supplies only one or two modalities.

Missing modalities are the default

Users may type only text, decline audio, upload a damaged image, or encounter an ASR failure. Train and evaluate every available modality combination.

Use modality dropout during training, pass presence and quality signals into fusion, and retain an auxiliary loss for every unimodal branch. At serving time:

Availability → Unimodal encoding → Quality estimation → Dynamic fusion
             → Multi-task prediction → Calibration → Policy decision

Report full-modality, unimodal, and missing-modality performance separately. A single Macro-F1 score with every channel present hides operational risk.

Data: self-report before observer inference

Emotion labels are subjective. Majority votes from external annotators describe how observers interpret an expression, not necessarily how the person feels.

Store self-reported labels and valence/arousal as primary supervision, observer-label distributions as soft labels, context, modality quality, consent scope, and provenance. Split train, validation, and test by user or speaker. Adjacent segments from one person, conversation, or source video must not cross splits.

Public datasets are useful for pretraining and baselines, but acted or scripted MELD and IEMOCAP samples differ from real Chinese users. The final test set must represent the target population, devices, languages, and missing-modality distribution with appropriate authorization.

Training in evidence-driven stages

  1. Establish text, speech, and image unimodal baselines.
  2. Freeze most encoders and train adapters, fusion, and multi-task heads.
  3. Add cross-modal attention, modality dropout, soft labels, and imbalance-aware loss.
  4. Apply LoRA/QLoRA or staged unfreezing only when evaluation justifies it.

LoRA rank, target layers, learning rate, and quantization are experiment variables, not fixed recipes. Small data can easily damage foundation capabilities if every encoder is tuned indiscriminately.

L = λ1 · MultiLabelEmotionLoss
  + λ2 · ValenceArousalRegressionLoss
  + λ3 · UnimodalAuxiliaryLoss
  + λ4 · CrossModalConsistencyLoss
  + λ5 · CalibrationLoss

Track dataset and split versions, seeds, preprocessing, base models, code commits, hyperparameters, and hardware. The registry should contain weights, thresholds, calibrators, and a Model Card—not just a merged checkpoint.

Evaluation beyond accuracy

Report Macro-F1, per-class precision and recall, confusion matrices, and multi-label metrics; MAE and CCC for valence/arousal; and ECE, Brier score, and selective-risk curves for confidence.

Evaluate modality ablations, damaged and missing inputs, demographic and device slices, cross-dataset/OOD transfer, calibration and abstention, and safety behavior. A headline accuracy without split details, confidence intervals, class distribution, and independent repetition is not meaningful.

The Xinxu data, training, and evaluation lifecycle

Serving needs policy before and after the model

Consent and purpose check
  → File safety, type, and size limits
  → Modality quality and preprocessing
  → Parallel encoders, fusion, and multi-task prediction
  → Calibration, thresholds, and OOD detection
  → Safety policy and response generation
  → User confirmation or correction

Validate output against a strict schema. Parse failure must return an error or explicit degraded result, never silently default to neutral. Apply deadlines, batching limits, GPU admission control, and circuit breakers, and observe ASR, encoder, fusion, and generation latency separately.

LoRA adapters do not always need merging. Merging fits a single low-overhead deployment; separate adapters fit multiple variants and fast rollback. Verify that the chosen inference engine actually supports the target multimodal architecture and adapter configuration.

Safety, privacy, and product boundaries

Voice and images can contain biometric and sensitive information. Minimize collection, encrypt transit and storage, shorten retention, support deletion, isolate purposes, and audit access. Low-confidence samples must never enter a training pool without separate, explicit training consent.

Use language such as “emotional cues” or “model hypothesis,” not claims of reading inner states. Do not use the system for employment, performance, education, insurance, credit, or law-enforcement decisions, and do not present it as a mental-health diagnostic tool. The EU AI Act restricts emotion inference in workplaces and educational institutions and highlights limitations in reliability, specificity, and generalization.

Self-harm or immediate-danger language requires a separately designed safety path, not a conclusion from an emotion label: supportive language, encouragement to contact local emergency services or a trusted person, and human escalation where lawful and consented.

Monitor model behavior, not people's emotions

Monitor modality availability and quality, prediction entropy, calibration error, abstention, user correction, slice performance, drift, tail latency, and resource saturation. Fixed targets such as neutral under 40% or average confidence above 0.75 are not universal health indicators. Confidence can be wrong, and real population distributions change.

Retraining should follow evidence: degradation on a stable target set, widening group gaps, new devices or languages, or a changed label definition. Release through offline gates, shadow traffic, and a small canary with rapid rollback.

The architectural conclusion

Xinxu Cloud Brain AI is not merely Qwen plus Whisper plus a classifier. It is a multimodal learning system centered on evidence quality, consent, and uncertainty.

Text says what was expressed; speech contributes how it was expressed; drawings add personal visual context. Fusion combines evidence without forcing certainty. A reliable system lowers confidence when signals conflict, degrades safely when modalities are missing, respects user corrections, and never confuses an affective cue with a diagnosis.

References

34 / 41

Modern Concurrency: From Execution Models to Production Reliability

A practical guide to memory models, Java virtual threads, Go channels, Python asyncio, structured concurrency, backpressure, cancellation, and observability.
Aug 07, 2026 · 15 min read

Concurrency programming can be reduced to three questions: How do we divide work? How do tasks coordinate? How do we protect shared state? Languages and runtimes provide threads, coroutines, event loops, actors, and channels, but none of these tools creates correctness by itself.

Production systems must also control task lifetime, resource limits, deadlines, cancellation, backpressure, and partial failure. When these constraints are designed together, concurrency becomes a system that can be reasoned about, observed, and controlled—not merely a way to run several tasks at once.

A 2026 decision map for concurrency

Separate the concepts first

Concurrency means tasks overlap in time and require coordination. Parallelism means tasks actually execute simultaneously. A single-core event loop can be concurrent without executing Python code in parallel; workers on multiple cores may provide both.

Synchronous/asynchronous and blocking/non-blocking are also independent axes. The former mostly describes control flow and result delivery. The latter asks whether a calling thread stops when an operation cannot complete immediately. A non-blocking call may return a status, handle, or incomplete result—not the final value. An asynchronous API may still block an internal worker.

Processes provide separate address spaces and fault boundaries. Platform threads share process memory and are scheduled by the operating system. Goroutines, Java virtual threads, and asyncio Tasks are lighter runtime-managed units, but they do not share one implementation or one set of semantics.

A coroutine is not atomic. It yields at suspension points; two tasks performing read-modify-write operations around an await can still violate an invariant. A single event-loop thread removes simultaneous multicore execution, not logical races.

Memory models are about ordering

Concurrency bugs commonly arise from visibility, atomicity, and ordering. The reliable way to reason about them is through the language memory model and explicit happens-before relationships, not assumptions about a particular CPU cache.

Java defines volatile through its memory model: a write to a volatile field happens-before a subsequent read, establishing visibility and ordering. It does not make a compound operation such as count++ atomic. Locks, thread start and termination, and concurrent collections establish other ordering guarantees.

Go has different syntax but the same fundamental requirement. Concurrent access to one location with at least one write must be serialized using channels, sync, or sync/atomic. The official memory model gives excellent advice: if proving correctness requires clever memory-model reasoning, simplify the design.

Locking rules should be protocols, not slogans. Multiple locks, lock striping, read-write locks, and optimistic concurrency can all be correct. Every shared state item needs a clear and consistently followed protocol; when several locks protect one invariant, acquisition order and deadlock behavior must be proven.

Java: virtual threads change cost, not correctness

Virtual threads became final in Java 21. High-throughput I/O services can dedicate a virtual thread to a task while retaining straightforward blocking code and useful stack traces.

Virtual threads do not accelerate CPU-bound work beyond available cores. They also should not be pooled merely to limit concurrency. Limit the scarce resource—database connections, remote API quota, memory, or file descriptors—with semaphores, connection pools, and rate limiters.

var permits = new Semaphore(64);

try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
    var future = executor.submit(() -> {
        permits.acquire();
        try {
            return callDownstream();
        } finally {
            permits.release();
        }
    });
    return future.get();
}

Structured Concurrency remains a preview API in Java 25, so its surface should not be presented as permanently finalized. Its direction is what matters: parents own children, scope exit waits for subtasks, sibling work can be cancelled after failure, and deadlines and errors follow a task tree rather than leaving orphaned Futures.

Go: a channel is not a durable message broker

Goroutines and channels still provide a concise way to express collaboration. A matched send and receive, or closing a channel and observing that close, establishes synchronization. Channels carry ordering as well as values.

“Share memory by communicating” does not mean every counter requires a channel. Channels are natural for work streams and ownership transfer. A mutex is often clearer for a short critical section around one object; atomic operations fit small counters and flags.

Production code must answer questions tutorials often omit: Who closes the channel? What is its capacity? What happens when consumers slow down? How does cancellation propagate? How do goroutines exit? Pass deadlines and cancellation through context.Context, and keep queues bounded so “asynchronous” does not become unbounded memory growth.

Python: the GIL is no longer a one-line answer

With default CPython, asyncio or threads remain appropriate for I/O-bound work, while CPU-heavy pure Python commonly uses processes, native extensions, or external compute. Since Python 3.13, however, CPython has offered an optional free-threaded build that can run Python threads in parallel across cores. It is not automatic acceleration: some extensions may re-enable the GIL, and both safety and performance require testing.

For asynchronous programs, asyncio.TaskGroup expresses a group of tasks that share a lifetime. Leaving the scope waits for all children; the first non-cancellation failure cancels siblings and errors are aggregated. Cleanup belongs in try/finally, and CancelledError should normally be re-raised after cleanup.

async def load_dashboard():
    async with asyncio.TaskGroup() as group:
        user = group.create_task(load_user())
        projects = group.create_task(load_projects())
    return user.result(), projects.result()

Never run blocking I/O directly on the event loop. Use an asynchronous driver or isolate legacy blocking code with asyncio.to_thread(). Route CPU work to a process pool, a tested free-threaded runtime, or an external executor according to the deployment.

From concurrency models to structured lifetimes

Reactor, Future, Callback, Actor, and CSP still solve different problems. Reactor dispatches ready events. Future represents a later value. Actor contains mutable state behind a mailbox. CSP coordinates processes through communication.

The major modern addition is that task lifetime must be modeled too. A concurrent task should not outlive the request that created it unless ownership is deliberately transferred. Structured concurrency forms a task tree so completion, failure, cancellation, and observability have explicit parent-child relationships.

The production concurrency loop

Eight production rules

  1. Define the objective: lower latency, higher throughput, and fault isolation require different designs.
  2. Classify the work: measure I/O wait, CPU work, and mixed workloads before choosing concurrency.
  3. Limit work in flight: lightweight tasks do not make connections, memory, descriptors, or quotas infinite.
  4. Bound every queue: capacity, overflow policy, priority, and discard semantics must be explicit.
  5. Propagate deadlines and cancellation: timeout is an end-to-end budget, not a collection of unrelated timers.
  6. Assign state ownership: prefer immutable values, single writers, and transfer; synchronize shared mutable state.
  7. Observe failure and saturation: record queue delay, in-flight work, rejection, cancellation, lock contention, and downstream latency—not just average QPS.
  8. Test interleavings: use race detectors, stress tests, fault injection, and virtual time to cover timeout, cancellation, retry, and partial failure.

Patterns still matter—with limits

Immutability remains the best default for reducing shared-state complexity. Copy-on-write fits read-mostly data, not large collections with frequent writes. Thread-local state requires cleanup with reused platform threads and should not become a large cache on virtual threads. Worker pools are valuable for resource isolation and capacity control, not merely thread reuse.

Producer-consumer is particularly easy to misuse. A queue decouples producers and consumers, but it cannot eliminate a sustained rate mismatch. If average production exceeds consumption, a bounded queue fills and an unbounded queue exhausts memory. The solution is backpressure, rejection, degradation, scaling, or less input—not another queue.

A 2026 definition

Concurrency programming is not simply “doing more things at once.” It is managing the lifetime and state relationships of multiple tasks under finite resources and uncertain failure.

Threads, locks, channels, and execution models are implementation tools. Scopes, cancellation, backpressure, resource budgets, and observability are equally important parts of the design. Concurrency is easier to create than ever; that makes constrained concurrency more important than ever.

A mature system is not one that can start a million tasks. It knows how many should start, when they must stop, whose failure affects whom, and how overload remains controlled.

References

35 / 41

Do Large Language Models Understand? From Next-Token Prediction to the Boundary of Intelligence

Large language models are built from next-token prediction and matrix operations. Does that mean they understand nothing? A careful distinction between representation, functional understanding, consciousness, and human experience.
Aug 07, 2026 · 15 min read

When a model can write code, review a contract, explain quantum mechanics, and discuss whether it is conscious, we instinctively imagine a mind behind the screen.

Yet when we open the system and follow the computation downward, we do not find desire, experience, or intention. We find tokens, vectors, parameters, attention weights, and repeated matrix multiplication.

This creates an uncomfortable question:

If a large language model is fundamentally predicting the next token, does it understand anything—or is it merely excellent at producing answers that look like understanding?

My original position was blunt: modern language models are not real intelligence. They predict the next character using probability. More parameters, more layers, and attention still reduce to matrix operations; the program itself understands nothing.

That intuition captures an essential technical fact, but it is too absolute. My revised position is:

Large language models are artificial intelligence and exhibit a form of functional understanding. But there is no convincing evidence that they possess human subjective consciousness, a persistent self, or intrinsic intention. Complex internal representations are not the same as living in and experiencing the world.

The model predicts tokens, not characters

Modern language models generally predict the next token, not the next character. A token may be a Chinese character, part of an English word, punctuation, a number, or a fragment of code.

How a language model generates the next token

Autoregressive generation can be summarized as:

existing context
→ tokenization
→ embeddings
→ many Transformer layers
→ probability distribution over the vocabulary
→ select one token
→ append it to the context
→ repeat

Given “Paris is the capital of,” the model does not simply retrieve the next word from a record. It calculates a probability for every candidate token. Sampling rules then select the actual continuation.

The GPT-4 technical report explicitly describes GPT-4 as a Transformer-style model pre-trained to predict the next token in a document. That is not an insult. It is the core training objective.

At the bottom, it really is matrix computation

At the physical execution level, language-model inference consists largely of:

  • vector and matrix multiplication;
  • similarity calculations inside attention;
  • softmax normalization;
  • feed-forward networks;
  • nonlinear activations;
  • residual connections and normalization.

Self-attention projects each token representation into Query, Key, and Value vectors:

Q = XWq
K = XWk
V = XWv

Attention(Q, K, V)
= softmax(QKᵀ / √d) V

The computation estimates which positions in the context matter for the current representation and how much information should be combined from them.

“Attention” is a mathematical name, not evidence that the model focuses as a person does. Artificial “neurons” are computational units inspired by biology, not equivalents of brain cells.

After dozens or hundreds of layers, the model produces probabilities for the next token. The GPU is indeed executing linear algebra at enormous scale.

But “it is only math” does not settle the question

It is tempting to stop here and conclude that the model understands nothing. The problem is that a low-level description does not automatically negate a high-level capability.

A music file is binary data. A computer is transistor state. Human neural activity can be described through electrochemical events. We do not conclude that melody or human understanding is unreal merely because the substrate follows physical rules.

The better question is not whether the substrate is mathematics. It is:

What internal organization emerges from the computation, and what can the resulting system do reliably?

This is what the “stochastic autocomplete” description often leaves out.

Prediction forces the model to learn structure

A tiny model trained on a few sentences may only memorize local correlations. Predicting the next token across a vast and diverse corpus is much harder.

To reduce prediction error, a model must capture some combination of:

  • grammar and reference;
  • relationships among entities, properties, and events;
  • document structure and style;
  • syntax, types, and dependencies in code;
  • patterns linking problems to solutions;
  • world knowledge and causal regularities reflected in language.

To complete “The glass fell from the table onto concrete, so it probably…,” a useful model benefits from representing that glass is brittle, gravity moves objects downward, and impact can cause breaking. Those ideas may not be stored as human-readable propositions, but distributed representations can support the prediction.

“It predicts the next token” and “it learns complex representations” are therefore compatible. The prediction task is difficult enough to force compression of many structures expressed through language.

Scale is not the whole explanation

Model capability comes from more than parameters and depth.

FactorContribution
Parameter scaleCapacity to store and combine patterns
DepthMultiple stages of transformation and abstraction
AttentionContext-dependent information composition
Training dataThe language, knowledge, and biases available to learn
ObjectiveWhat behavior training rewards
Post-trainingInstruction following, preferences, safety, and tool behavior
Inference-time computeSearch, verification, and additional reasoning steps
External toolsCurrent facts, calculation, memory, and action

Parameters are not database rows. A fact or capability is usually distributed across many weights, and one weight participates in many patterns.

Calling every advance “more GPUs and parameters” is as reductive as calling the model a digital brain.

“Understanding” hides three different questions

Disagreement persists because people use one word for different phenomena.

1. Statistical and structural understanding

Can the system identify relationships and regularities in language and other representations?

Modern models clearly do this well. They recognize semantic similarity, transform expression, follow code dependencies, and extract contractual structure.

2. Functional understanding

Can the system apply concepts to new tasks, explain relationships, revise a plan after feedback, and complete work that historically required cognition?

In many domains, models demonstrate some functional understanding. A model may enter an unfamiliar repository, locate a defect, and propose a valid patch. Its probabilistic substrate does not erase the functional result. Weather forecasting is probabilistic too, yet it can represent something real and useful.

3. Subjective understanding

Does the system know that it is reasoning? Does it have experience, intention, desire, a stable self, or a first-person point of view?

Here, we have no reliable evidence that language models understand as humans do.

A model saying “I am afraid of being shut down” does not prove the experience of fear. The statement may simply be probable in context. A model can describe pain precisely without injury, nerves, or the experience of suffering.

The careful conclusion is:

Language models display structural understanding and some functional understanding, but have not been shown to possess subjective understanding.

Form is not the same as meaning

Bender and Koller distinguish linguistic form from meaning. Text training exposes a model to forms and their relationships. Human meaning also involves connections among language, communicative intent, shared environments, and lived experience.

A person understands “fire is hot” not only by reading the sentence but perhaps by approaching heat, feeling danger, receiving warnings, and acting differently in the world.

A text-only model has no such life history. Multimodal training exposes models to images, sound, and video. Robotics and tools provide limited environmental feedback. But sensor input alone does not establish human embodiment or subjective experience.

This helps explain why a model can offer a profound explanation and then fail on a trivial variation. Its representations can be broad and powerful without being consistently grounded in the world.

Modern AI systems do more than pre-training

“A next-token predictor” accurately describes the foundation, but not the entire modern system. Current systems can also include:

  • supervised fine-tuning and preference optimization;
  • reinforcement learning and verifiable rewards;
  • inference-time search and comparison;
  • retrieval-augmented generation;
  • code execution, calculators, and business tools;
  • multimodal input and environmental feedback;
  • agent state, planning, approval, and outcome evaluation.

These additions usually do not turn the model into an intentional subject. They provide a probabilistic model with external memory, sensors, actuators, and verification.

A model does not develop a desire to email a customer because an email tool exists. The objective comes from people, system instructions, or reward. Permission comes from policy. Correctness still depends on evidence and feedback.

Why does it hallucinate so confidently?

Next-token training explains a characteristic failure: the model is optimized first to produce a plausible continuation, not to guarantee that every sentence corresponds to a verified fact.

When knowledge is missing, continuing to generate often remains more natural than remaining silent. A model may invent a paper, API, or event in fluent language.

Reliable production systems therefore need more than eloquence:

model proposes a judgment
→ retrieval or tools obtain facts
→ structured constraints
→ rules and evaluations verify
→ people approve high-risk actions

Treating the model as a probabilistic reasoning component is more realistic than treating it as an omniscient digital employee.

Is it “real AI”?

If artificial intelligence means machines completing tasks normally associated with human intelligence, language models clearly qualify. Claiming that they are “not AI” is difficult to defend technically.

But if real intelligence requires a persistent self, embodied experience, self-generated goals, intrinsic concern for consequences, and subjective consciousness, current language models are far from demonstrated human-like intelligence.

The issue is not a binary choice between fully intelligent and entirely unintelligent. Intelligence may be multidimensional: language, prediction, planning, learning, embodiment, social relationship, selfhood, and consciousness are not the same faculty.

Language models are extremely strong along some dimensions. Along others, convincing evidence is absent.

My final position

I would no longer say, “It is only probability, so it understands nothing.” The word “only” hides the complexity that large-scale prediction can produce.

I would also not say that a model understands the world as a person does. Fluent language invites us to project a subject, emotions, and intentions onto software without sufficient evidence.

My position is:

A large language model is a high-dimensional probabilistic system trained through enormous amounts of data and computation. Its core objective is token prediction, and its substrate is matrix computation. In pursuing that objective, it develops internal representations that support language, knowledge, reasoning, and tool use. It can exhibit functional understanding, but has not been shown to possess human subjective consciousness, a stable self, or intrinsic intention.

This does not diminish its usefulness. An airplane need not flap like a bird to fly. A machine may not need to reproduce the human brain to perform valuable intelligent work.

But we must know what the system is in order to decide how much to trust it, which authority to grant it, and which responsibilities must remain human.

The most interesting question may no longer be whether it resembles us. It may be:

When a probabilistic system without demonstrated consciousness can perform an expanding range of intelligent tasks, do we need to redefine intelligence itself?

Further reading

36 / 41

LangChain vs. LlamaIndex: Choosing a Framework and Building Production RAG

A practical comparison of LangChain, LangGraph, and LlamaIndex, followed by an end-to-end production RAG architecture covering ingestion, hybrid retrieval, reranking, citations, evaluation, and access control.
Aug 07, 2026 · 16 min read

LangChain and LlamaIndex are often compared because both connect models, vector stores, data sources, and tools, and both can build RAG systems and agents. “LangChain for agents, LlamaIndex for RAG” is a useful first approximation, but it is no longer a complete technical conclusion.

LangChain is now primarily a high-level agent framework with model, tool, middleware, and prebuilt agent-loop abstractions. LangGraph provides the lower-level runtime for durable, stateful orchestration. LlamaIndex remains data-centric, organizing Document, Node, Ingestion Pipeline, Index, Retriever, and Query Engine abstractions, while also offering Workflows, FunctionAgent, ReActAgent, and multi-agent coordination.

The useful question is not which framework is universally stronger. It is: Does the system's dominant complexity live in data ingestion, retrieval, and evidence assembly, or in tool decisions, state transitions, and long-running orchestration?

The capability boundaries of LangChain, LangGraph, and LlamaIndex

A more accurate comparison

DimensionLangChain / LangGraphLlamaIndex
Core positionAgent framework plus a stateful orchestration runtimeContext engineering, indexing, retrieval, querying, and data workflows
Main abstractionsModel, Tool, Middleware, Agent; State, Node, EdgeDocument, Node, Transformation, Index, Retriever, Query Engine, Workflow
RAGLoaders, splitters, vector stores, retrievers, and 2-step/agentic/hybrid RAGConcentrated abstractions for ingestion, metadata, indexing, retrieval, synthesis, and evaluation
AgentsHigh-level create_agent; LangGraph adds persistence, streaming, human review, and recoveryFunctionAgent, ReActAgent, AgentWorkflow, and event-driven Workflows
Best-fit complexityTool use, branches, loops, approvals, and long-lived stateHeterogeneous data, sophisticated retrieval, document relationships, and data agents
Selection signalBusiness workflow and agent behavior dominatePrivate-data quality and retrieval behavior dominate

LlamaIndex is not merely a vector-index utility, and LangChain is not merely the old idea of joining chains. Both cover overlapping territory. Choose the abstractions that match the part of the system your team must change and debug most often.

Choose by scenario

For an enterprise policy or documentation assistant, the hard problems are usually parsing, versions, permissions, section structure, hybrid retrieval, citations, and evaluation. LlamaIndex is often a natural center because its ingestion and retrieval concepts are cohesive.

For a support agent that reads knowledge, queries orders, checks refund rules, requests approval, calls payments, and records an outcome, the core is a recoverable state machine with controlled tool access. LangChain agents on LangGraph fit that shape well.

For research and report generation, LlamaIndex can expose a high-quality retrieval tool while LangGraph manages research steps, state, approval, and recovery. Keep the boundary explicit: the orchestration layer consumes a structured Retriever or Query Engine result rather than manipulating index internals.

If retrieval itself is the product—tenant-aware filters, temporal rules, parent-child retrieval, graph retrieval, or multimodal documents—build the data layer as a platform and expose it as a tool to whichever agent framework you use.

RAG has two lifecycles, not one arrow

Offline ingestion:
Sources → Parse/OCR → Clean and restore structure → Metadata/ACL → Chunk
        → Embeddings + lexical index → Versioned storage → Quality checks

Online serving:
Question → Safety/ACL → Query understanding → Routing → Hybrid retrieval
         → Fusion/dedup → Rerank → Context assembly → Grounded generation
         → Citation validation/abstention → Response

Continuous loop:
Real queries + labeled set → Retrieval eval → Generation eval
                           → Production monitoring → Failure replay

An end-to-end production RAG pipeline

A diagram containing only documents, embeddings, a vector database, and an LLM omits the production failure points: versions, authorization, query rewriting, lexical recall, reranking, context budgets, citation mapping, abstention, and evaluation.

1. Ingestion: make the evidence correct first

PDF ingestion must preserve more than continuous text. Heading hierarchy, page anchors, tables, captions, footnotes, and reading order can determine whether an answer is correct. Scanned files need OCR. LlamaParse, Unstructured, and specialized parsers are candidates, but evaluate them on your corpus for field completeness and layout fidelity.

Every document should carry a stable ID, source, tenant, ACL labels, version, effective period, update time, section path, page/anchor, and content hash. Updates and deletions must remove obsolete chunks precisely. An ingestion job should be idempotent, replayable, versioned, and observable.

2. Chunking: there is no universal 512-1024 setting

A fixed token window is a baseline, not a best practice. Good size depends on document structure, query type, the embedding model, and generation context.

  • Split first on semantic boundaries such as headings, paragraphs, lists, code, and tables.
  • Preserve document, section, page, and adjacency relationships.
  • Retrieve small chunks for precision, then expand to parent sections or neighbors for generation.
  • Give tables, code, and FAQs specialized strategies.
  • Run ablation tests over size, overlap, and parent-child retrieval on real questions.

Overlap reduces boundary loss but increases index size, duplicate retrieval, and context waste. Increase it only when evaluation shows a measurable benefit.

3. Embeddings and indexes: semantic search is one channel

Choose embeddings by language, domain, dimensions, cost, and deployment constraints. Multilingual models such as BGE-M3 can be candidates, not automatic answers. Build a retrieval set containing bilingual text, abbreviations, product names, identifiers, rare entities, and hard negatives; measure Recall@K, MRR, or NDCG.

Most systems benefit from at least dense vector search and BM25/lexical search. Dense retrieval handles paraphrase; lexical retrieval handles identifiers, error codes, names, and rare exact terms. Fuse rankings with a stable method such as Reciprocal Rank Fusion.

Apply tenant, ACL, validity-time, and data-type filters before or during retrieval. Never retrieve unauthorized content and merely remove it before prompting.

4. Query understanding and routing

Conversation turns may need rewriting into standalone queries. Complex questions may need decomposition. Identifier-heavy searches should favor lexical retrieval. Structured facts should route to SQL or an API rather than being forced through a vector store.

Rewriting must preserve user constraints. Record the original query, rewritten query, route, and model version so failures can be localized.

5. Recall, fusion, and reranking

First-stage retrieval optimizes coverage and can gather a larger pool across channels. A cross-encoder or specialized reranker then scores query-document pairs before evidence enters the context.

Top-K = 3-5 is not a universal rule. Retrieve broadly, rerank and deduplicate, then cut dynamically by token budget, score threshold, and evidence coverage. A reranker improves relevance; it does not enforce authorization, validity, or business eligibility. Deterministic filters own those constraints.

6. Context assembly

Do not concatenate raw Top-K chunks. Deduplicate them, restore section paths, merge useful neighbors, retain stable source IDs, control per-document dominance, and avoid cutting critical statements at a token boundary.

Each evidence block should carry a citation ID, title, safe source URL or filename, page/anchor, update time, and text. Ask the model to ground claims, separate fact from inference, abstain when evidence is insufficient, and return parseable citations.

A prompt alone cannot eliminate hallucinations. Abstention needs retrieval confidence, evidence coverage, and post-generation citation validation.

7. Generation, citations, and abstention

Support three outcomes: answer with sufficient evidence; answer the supported portion and disclose gaps; or abstain and request clarification or human review.

Validate that every material claim has a citation and that the cited passage actually entails it. High-risk workflows may require deterministic rules, structured-output validation, or approval. Store retrieval traces, scores, filter reasons, versions, cost, and latency internally without exposing sensitive metadata to users.

8. Evaluation

LayerMetricsQuestion
Parsing/chunkingField completeness, structure fidelity, chunk coverageDid valid evidence enter the index?
RetrievalRecall@K, MRR, NDCG, filter accuracyWas the evidence found and ranked well?
GenerationFaithfulness, citation accuracy, completeness, abstention accuracyIs the answer supported?
SystemP50/P95 latency, cost, errors, cache hit rate, ACL violationsIs it safe and operable?

Use real questions, including unanswerable, ambiguous, cross-document, version-conflict, and permission-isolation cases. Online feedback should include reformulation, repeated queries, escalation, and citation clicks—not just thumbs-up rates.

When to combine the frameworks

A clean hybrid exposes LlamaIndex ingestion and retrieval as a typed knowledge-search tool. LangGraph decides when to retrieve, whether to refine the query, which other tools to call, and how to handle approval and recovery.

LangGraph / LangChain Agent
  ├─ KnowledgeSearchTool → LlamaIndex Retriever / Query Engine
  ├─ SQLTool
  ├─ BusinessAPITool
  └─ HumanApproval

Do not combine them for a fixed two-step RAG application that one framework can express in a few dozen lines. Two abstraction stacks create version, tracing, type-conversion, and debugging costs. Separate them only when the data layer and orchestration layer have independently earned that complexity.

Final guidance

  • Knowledge Q&A dominated by complex documents and retrieval: start with LlamaIndex.
  • Multi-tool agents dominated by branches, loops, persistent state, and approvals: start with LangChain + LangGraph.
  • Simple two-step RAG: either framework—or a model SDK plus a search service—is sufficient.
  • Complex data agents: LlamaIndex for evidence, LangGraph for task lifetime is a clear but optional boundary.

Framework choice affects developer experience, not the quality ceiling by itself. Evidence quality, authorization, retrieval and reranking, abstention, evaluation, and observability decide whether RAG is production-ready.

References

37 / 41

How to Build an AI-Native Team

AI-native transformation is not a tool rollout. It is a redesign of goals, workflows, human-agent responsibilities, organizational context, evaluation, and governance.
Aug 06, 2026 · 15 min read

Whenever I set up a new computer, I restore the development environment first: GoLand, IntelliJ IDEA, VS Code, multiple JDKs, Python, Node.js, MySQL, Redis, Docker, Postman. For years, that ritual took two or three days and felt like part of being an engineer.

This time I stopped halfway through.

When had I last opened GoLand? Or IntelliJ? I was still building software, so why had the tools that once defined my productivity stopped being my primary entry point?

The answer was not that coding had disappeared. The structure of the work had changed. I increasingly described the outcome, supplied context, reviewed a plan, and verified the result. An agent explored the repository, changed the code, ran the tests, and handled repetitive execution. Languages and frameworks still mattered, but they were moving from tools I had to operate personally to environments I had to understand and judge.

The change was not the IDE. It was the basic unit of work.

The same shift will not remain confined to engineering. Once AI can interpret context, use tools, execute multi-step tasks, and revise a plan based on results, the organizational question is no longer “Should we give employees an AI assistant?” It becomes:

How should we redesign a team when AI can participate as an execution unit?

AI-enabled is not AI-native

Many AI transformations begin with software procurement and end there. Meeting notes are summarized, marketing copy is generated, developers receive code completions, and leadership sees adoption numbers rise. The company declares itself AI-powered.

Yet the operating system remains unchanged. Tasks still move through layers of handoffs. Context remains trapped in chat threads. Decisions still wait for meetings. People remain the transport layer between every step, while AI merely makes a few local steps faster.

An AI-native team is designed with AI as a first-class participant from the beginning.

AI-enabledAI-native
Adds an assistant to the old processRedesigns the process around the outcome
AI drafts; people keep moving the workAI executes within explicit authority
Prompts are personal tricksContext is an organizational asset
Measures users and callsMeasures quality, cycle time, cost, and outcomes
Humans repair errors after the factEvaluation, permissions, and escalation are designed upfront
Knowledge lives in documents and headsKnowledge is retrievable, versioned, and reusable

A useful test is:

If AI disappeared from a core workflow tomorrow, would the process become slower, or would it have to be redesigned?

If it only becomes slower, the organization is probably AI-enabled. If the process can no longer operate in the same way, AI has entered the organizational architecture.

A new operating system for work

AI-native transformation is not a single tool project. Five layers must evolve together.

The five-layer operating system of an AI-native team

1. Goals

Agents can execute the wrong objective with remarkable efficiency. Clear objectives therefore matter more, not less. Every task needs a problem, an intended beneficiary, constraints, and an acceptance test.

“Research our competitors” is not an executable goal. “Compare the pricing, core workflow, and recurring customer complaints of three competitors; recommend two differentiated hypotheses for next quarter; cite the evidence behind every conclusion” is much closer.

2. Workflows

Do not insert AI into one step of an inherited process. Work backward from the result. Which steps can run in parallel? Which information can be collected automatically? Which judgments must remain human? How does the process degrade? Where does the output go next?

3. Human-agent responsibilities

The useful question is not “Which job will AI replace?” It is “How should judgment and execution be divided inside each job?”

People are better positioned to own goals, taste, priorities, responsibility, relationships, and high-risk exceptions. Agents are well suited to search, synthesis, generation, batch execution, state synchronization, and work with clear verification.

4. Organizational context

If every new session requires an explanation of the company, product, vocabulary, and past decisions, the organization has hired an intern who loses all memory every morning. Principles, domain language, processes, decisions, examples, counterexamples, and permissions must become retrievable and maintainable context.

5. Evaluation and governance

Without evaluation, the team can only say that work “feels faster.” Without governance, greater speed merely spreads errors faster. Evaluation, authorization, audit, fallback, and human escalation must be designed with the workflow.

The new unit of work: a person with agents

Traditional organizations expand execution capacity by adding people and layers. Information moves through managers, and coordination cost rises with headcount.

An AI-native organization increasingly consists of small human-agent units. A product leader may coordinate research, data, prototyping, and writing agents. A developer may delegate repository exploration, implementation, testing, and documentation. The person retains ownership of the objective while agents multiply execution bandwidth.

The goal, execution, verification, and learning loop between people and agents

The important variable is not the number of agents. It is clarity of responsibility:

Human: define goals → set boundaries → review critical judgment → own the outcome
Agent: plan → use tools → execute → submit evidence
System: record → evaluate → enforce permissions → trigger escalation

Teams often build a complex multi-agent network too early. A deterministic workflow is usually more reliable when the task is well understood. A single agent should remain a single agent unless the work genuinely benefits from parallel specialization, context isolation, or independent verification.

Complexity should be pulled by the problem, not pushed by enthusiasm for the technology.

Six principles for AI-native workflows

1. Begin with acceptance criteria

Define “done” before choosing a model. Tasks with objective checks are strong candidates for agentic execution: tests pass, accounts reconcile, required fields exist, citations resolve, or outputs match a schema.

2. Treat context as capital

Better output comes not only from better models but from better context. Context should be managed like code: sourced, versioned, owned, reviewed, and retired when stale.

3. Prefer simple, composable patterns

Use deterministic code for deterministic work. Invoke models where ambiguity requires judgment. Give an agent control of the path only when the path cannot be known in advance. Simpler systems are easier to evaluate, debug, and operate economically.

4. Match autonomy to risk

Low-risk, reversible, verifiable actions can run automatically. High-risk, externally visible, or irreversible actions require approval. Drafting an internal summary and sending a contract to a customer should not share the same autonomy level.

5. Make every step observable

At minimum, record the objective, context used, model and tool versions, critical decisions, result, human intervention, latency, and cost. Without traces, every production failure becomes guesswork.

6. Turn failure into system memory

If a human correction does not become a rule, example, or evaluation case, the same failure will return. Organizational learning begins when an individual correction becomes a reusable system capability.

Do not begin with an isolated “AI department”

When transformation is delegated entirely to an innovation unit, one of two things usually happens. The innovation team builds impressive demos without understanding operational constraints, or business teams treat AI as somebody else's project and refuse ownership of the outcome.

A healthier structure has four sources of accountability.

Leadership owns direction and boundaries

Leadership defines why the organization is changing, which workflows matter, what risks are acceptable, and how much capacity will be committed. Leaders also need to use the new operating model themselves. A company notices quickly when executives demand AI adoption while continuing to manage through meetings and cascading status reports.

A platform team owns shared infrastructure

This team provides model access, tools, identity, permissions, knowledge, evaluation, cost controls, and safety foundations. It does not own every business use case. Its job is to eliminate duplicated infrastructure work.

Business teams own outcomes

The people closest to a process understand its decisions and edge cases. Business owners need authority to redesign the process and accountability for quality. Responsibility cannot be transferred to a model vendor or platform team.

AI champions accelerate learning

Practitioners inside real teams help colleagues cross the initial barrier, share successful and failed patterns, and connect business needs with platform capabilities. Champions are catalysts, not a new approval layer.

Choose the first workflow carefully

Do not begin with a company-wide mandate. Begin with a real workflow that is valuable and safe enough to learn from.

Prioritize work that is:

  1. Frequent: it occurs every day or week;
  2. Time-consuming: much of the effort is search, synthesis, copying, or coordination;
  3. Verifiable: output quality has a reasonably clear standard;
  4. Controllable: failure is inexpensive or can be intercepted before action.

A rough prioritization model is:

Opportunity = frequency × time per task × verifiability × reuse ÷ risk

Do not measure only minutes saved. Track first-pass acceptance, rework, escalation, total cost per accepted result, and end-to-end cycle time. Doubling generation speed while tripling review effort is not a win.

Move beyond a prompt library

Prompt templates are useful, but they are only the shallowest layer of organizational context. A mature context system includes:

Principles: how we make tradeoffs and what is prohibited
Domain language: products, customers, metrics, and terminology
Processes: steps, inputs, outputs, and owners
Decision history: what was chosen, rejected, and why
Examples: strong outputs and known failure modes
Tools and permissions: what an agent may read or change
Evaluation sets: real tasks, expected outcomes, and boundaries

This material requires maintenance. Stale context is often more dangerous than missing context because it creates consistently wrong behavior.

Organizational memory is also not equivalent to putting every file into a vector database. Different knowledge has different lifecycles and access levels. Product metrics may change daily, policies monthly, architecture decisions require version history, and personnel data requires strict isolation. Retrieval is the entry point; governance is the system.

Governance is a prerequisite for scale

When an agent can only draft text, the primary risk is content quality. When it can send messages, change production systems, move money, or update customer records, the risk becomes action.

Design permissions around actions rather than around tools:

PermissionExampleBehavior
Automatically allowedRead public sources, draft, run read-only analysisExecute and log
Conditionally allowedEdit internal docs, run tests, create ticketsExecute when rules pass
Approval requiredExternal communication, production changes, financial or contractual actionsReview plan and impact first
ProhibitedBypass audit, expand its own access, read unrelated sensitive dataHard block

Every consequential workflow also needs least privilege, complete auditability, an immediate stop mechanism, and a path back to a safe state.

Human escalation is not failure. The dangerous system is the one that does not know when to ask for help. When goals are ambiguous, evidence conflicts, authority is missing, risk rises, or verification fails repeatedly, the agent should pause and hand over completed work, evidence, and the unresolved decision.

A practical 90-day path

A 90-day roadmap for an AI-native team

Days 1–30: diagnose and ignite

  • Start with one business team, not the entire company.
  • Map three frequent workflows and record current quality, time, and cost baselines.
  • Standardize two or three tools and publish clear data boundaries.
  • Name a business owner, platform partner, and AI champion.
  • Redesign one low-risk, frequent, verifiable workflow.

Deliverables: baseline, use-case priorities, tool and data rules, and one working workflow.

Days 31–60: redesign process and context

  • Expand to three production workflows.
  • Define objectives, inputs, outputs, permissions, and escalation for each.
  • Build the minimum organizational context and a library of validated examples.
  • Convert real failures into the first evaluation set.
  • Review failures as openly as successes each week.

Deliverables: three workflow specifications, context v1, evaluation set v1, and a weekly quality report.

Days 61–90: institutionalize and expand

  • Run evaluations whenever models, prompts, tools, or workflows change.
  • Establish approvals, audits, cost controls, and incident response.
  • Compare cycle time, quality, cost, and escalation against the original baseline.
  • Extract reusable components instead of copying entire workflows.
  • Use evidence to expand, redesign, or stop each use case.

Deliverables: evaluation dashboard, governance mechanism, quarterly review, and the next roadmap.

Measure outcomes, not AI activity

The number of AI users and tokens consumed are adoption metrics, not value metrics.

More useful measures include:

  • Cycle time: from request to accepted outcome;
  • First-pass acceptance: results approved without rework;
  • Cost per accepted result: model, infrastructure, and human review combined;
  • Escalation rate: where people repeatedly need to intervene and why;
  • Recovery time: how quickly the workflow returns to a safe state;
  • Reuse: how often another team can adopt an existing capability;
  • Business outcome: revenue, conversion, quality, customer satisfaction, or risk reduction.

Metrics should not be used to prove that AI was the right decision. They should help the organization continuously decide what to automate, what to keep human, and what not to do at all.

AI-native organizations still amplify human judgment

AI will continue to reduce the cost of execution. It will not make the objective correct, create taste, or absorb accountability.

The best teams may not be those with the most agents. They will be the teams that define problems clearly, provide dense and accurate context, verify outcomes quickly, and convert failures into organizational memory.

An AI-native team is therefore not simply a team with fewer people.

It is a way to execute human judgment at greater scale while keeping the system controllable, verifiable, and accountable.

I did not install every old tool on that new computer. They had not become worthless. My role had moved from operating each tool to designing a system in which people and AI could work together.

For an organization, becoming AI-native begins with the same shift.

Further reading

38 / 41

From Query Understanding to Knowledge-Driven Recommendations

A production-minded architecture for tax and finance search, combining structured query understanding, hybrid retrieval, knowledge governance, reranking, and continuous evaluation.
Aug 06, 2026 · 13 min read

In 2022, I wrote a technical proposal for a new tax and finance product. It covered query understanding, recommendation systems, and the fragmentation of domain knowledge. Like many architecture documents of its time, it also contained a long inventory of techniques: tokenization, intent classification, collaborative filtering, ranking models, entity extraction, and knowledge graphs.

Most of those model names are no longer the interesting part. What has survived is a priority decision I made at the beginning:

Structured domain knowledge > query understanding > recommendation ranking.

Ranking is not unimportant. The point is that in a regulated, time-sensitive domain, a system's ceiling is first determined by whether it can represent the knowledge correctly, then by whether it can understand the user's actual situation, and only then by how well it orders the candidates.

If the knowledge base is incomplete, perfect query understanding still retrieves incomplete answers. If the system misses jurisdiction, entity type, or effective date, a more confident model can become more dangerous. If those foundations are weak, a sophisticated ranker merely amplifies errors with greater precision.

The evolution from a 2022 algorithm pipeline to a knowledge-driven retrieval system

Why tax and finance search is unusually difficult

General search often competes on relevance. Tax and finance search must satisfy several constraints at once.

Queries are underspecified. “Can a small business deduct this?” is not a complete business question. The legal entity, jurisdiction, transaction type, invoice type, and effective date may all change the answer.

Domain language is dense. A concept may have a statutory name, a professional abbreviation, an old name, and several everyday expressions. Words that look similar in general language may carry sharply different accounting or legal meanings.

Answers expire. Tax knowledge is not a static encyclopedia. Rules are issued, amended, superseded, and interpreted differently across jurisdictions. A relevant but expired result is not merely low quality; it is wrong.

Ranking is multi-objective. Users need authoritative source material, understandable explanations, and actionable guidance. The system must balance authority, relevance, readability, recency, and context.

Every conclusion needs provenance. A useful system may say that it lacks enough information. It should not produce a definitive answer that cannot identify its evidence and scope.

This is why a domain search product is not simply a vector database with an LLM on top. It is a decision system spanning knowledge governance, query interpretation, retrieval, ranking, and risk control.

Query understanding should produce a contract

Query understanding is traditionally divided into normalization, tokenization, correction, expansion, entity recognition, and intent classification. These capabilities still matter, but a production system needs a stable, auditable output that every downstream component can consume.

For example:

{
  "rawQuery": "How does a small business in Chongqing file VAT this year?",
  "normalizedQuery": "Chongqing small business 2026 VAT filing",
  "intent": "tax_policy_and_procedure",
  "entities": {
    "region": "Chongqing",
    "taxType": "VAT",
    "businessType": "small_business"
  },
  "constraints": {
    "effectiveAt": "2026-08-06",
    "authorityLevel": ["national", "municipal"]
  },
  "rewrites": [
    "Chongqing VAT relief for small businesses",
    "Chongqing VAT filing procedure for small businesses"
  ],
  "confidence": 0.91,
  "needsClarification": false
}

This object is more useful than an isolated label such as policy_lookup. Retrieval, filters, rerankers, answer generation, logging, and evaluation can share the same contract. When the system fails, the contract also helps reveal where it failed.

If a decisive constraint is missing, the right behavior is not to guess. A question about deducting the tax on a company vehicle may require the entity type, business purpose, invoice type, and transaction date. Asking one good clarifying question is safer than generating a polished answer to the wrong problem.

Replace algorithm inventories with an observable pipeline

My 2022 design used a Pipe-Filter architecture: a stable core moved data through plug-in algorithm components. The idea remains useful, but each stage now needs an explicit contract, evaluation method, trace, and fallback.

A production query pipeline for tax and finance search

A production pipeline can be organized as follows:

  1. Normalize input: characters, units, dates, regulation identifiers, jurisdiction aliases, and entity types.
  2. Detect safety and scope: sensitive input, unauthorized requests, and questions the product cannot answer.
  3. Parse intent and entities: task, business entities, temporal constraints, and jurisdiction.
  4. Rewrite and decompose: correct errors, map terminology, and split complex questions into verifiable subqueries.
  5. Retrieve through multiple channels: lexical, semantic, rule-based, graph-based, and behavioral retrieval.
  6. Fuse and deduplicate: combine channels without letting one scoring system dominate.
  7. Rerank under business constraints: semantic relevance, authority, validity, and user context.
  8. Validate confidence: evidence coverage, policy status, citation consistency, and conflicting sources.
  9. Present the result: answer, evidence, applicable scope, uncertainty, and next action.

Every stage should record its version, latency, input and output summaries, and failure reason. When a model times out, the system should degrade to lexical search and deterministic filters rather than taking the entire experience down.

Do not choose between lexical and vector retrieval

Domain search is naturally hybrid.

Lexical retrieval is excellent for regulation identifiers, technical terms, exact phrases, and numeric conditions. BM25 remains a strong baseline; embeddings do not make exact matching obsolete.

Vector retrieval handles conversational language and semantic similarity. A user may ask whether a hotel invoice from an employee trip is deductible while the source document discusses “input VAT deduction for accommodation services.” The expressions differ, but the underlying intent is close.

Rule-based retrieval enforces constraints that must not be approximated: jurisdiction, effective date, entity eligibility, and policy status.

Knowledge-relation retrieval follows connections such as policy, tax type, legal entity, transaction, required document, and filing procedure.

Behavioral retrieval becomes valuable only after sufficient, compliant, and de-biased feedback exists. Clicks are not proof of correctness; long dwell time may simply mean that the content is hard to understand.

The channels should not be concatenated blindly. A stable rank-fusion method can combine candidates before a reranker handles the top set. Fusion optimizes for coverage, reranking for order, and deterministic business rules protect boundaries that relevance must never override.

Recommendation is a decision process, not a model

The older proposal summarized industrial recommendation as “algorithm + recall + rank.” I would now make the stages explicit:

Candidate generation
  → eligibility filtering
  → deduplication and diversity
  → coarse ranking
  → fine reranking
  → policy constraints
  → explanation and presentation

Candidate generation answers “what might apply?” Filtering answers “what definitely does not apply?” Ranking decides “what should the user see first?” Explanation answers “why is this being shown?”

For tax and finance content, a conceptual score should include more than semantic relevance:

FinalScore =
  SemanticRelevance
  + AuthorityWeight
  + FreshnessWeight
  + ContextMatch
  + EvidenceCoverage
  - ExpirationPenalty
  - ConflictPenalty

This is not a universal formula. It is a design reminder: a semantically perfect result must still be rejected if it is expired, inapplicable to the jurisdiction, or unsupported by an authoritative source.

The knowledge layer sets the long-term ceiling

Knowledge fragmentation was the highest-priority problem in the original proposal. It matters even more now.

Acquire with provenance

Ingest regulations, official interpretations, procedural guides, cases, and internal material while preserving the source, publisher, publication date, effective period, and canonical URL.

Structure only what affects decisions

Do not begin with an enormous ontology. Start with the fields that determine applicability:

Policy → issuing authority
Policy → effective and expiration dates
Policy → jurisdiction
Policy → eligible entity
Policy → tax type
Policy → business event
Policy → supersedes or cites another policy

Resolve identity and conflict

The same policy may be mirrored, summarized, or incorrectly interpreted by multiple sources. The system must align entities, merge versions, apply authority rules, and expose conflicts instead of passing duplicate text to a model and hoping it decides correctly.

Evaluate and update incrementally

Knowledge needs a computable health state: source authority, current validity, field completeness, conflicts, and last review time. A policy update should trigger incremental ingestion and reevaluation of affected answers.

Where LLMs belong

LLMs have dramatically improved query interpretation, semantic rewriting, decomposition, and answer composition. They should not become an unconstrained center of the architecture.

Good uses include:

  • extracting structured intent and entities from conversational queries;
  • generating alternative retrieval expressions;
  • decomposing complex questions into verifiable subproblems;
  • semantically reranking retrieved evidence;
  • composing an answer from confirmed sources;
  • identifying conflicts and escalating them for review.

LLMs should not be the sole mechanism for:

  • determining whether a regulation is currently valid;
  • enforcing access control and data isolation;
  • authorizing high-risk business actions;
  • inventing missing facts when evidence is absent;
  • replacing deterministic jurisdiction, date, and eligibility filters.

The working rule is simple: let models handle ambiguity and systems handle certainty. Let models propose judgments, and let evidence and rules constrain them.

Evaluation compounds faster than model selection

Without an evaluation set, architecture work eventually becomes subjective tuning against a few impressive demo queries.

Build a layered evaluation set from real user questions:

LayerPrimary metricsQuestion
Query understandingIntent Accuracy, Entity F1Did the system understand the task?
RetrievalRecall@KDid the correct evidence enter the candidate set?
RankingNDCG@K, MRRDid the best result appear early enough?
AnswerEvidence coverage, citation accuracyIs every claim supported?
BusinessResolution rate, reformulation rate, escalation rateWas the problem actually solved?
RiskExpired citation rate, unauthorized access, unsupported claimsDid the system respect its boundaries?

The relevance flywheel from real queries to continuous improvement

The set must include ambiguous language, typos, multiple intents, cross-jurisdiction conflicts, expired rules, and contradictory sources. These cases—not the clean demos—determine whether a system is ready for production.

How I would start today

I would not begin by training a model or drawing an architecture with every known algorithm. I would proceed in this order:

  1. Select one bounded tax task and define correct outcomes and unacceptable errors.
  2. Build a minimal knowledge model around provenance, time, jurisdiction, entity, and policy relationships.
  3. Establish a hybrid BM25 and vector retrieval baseline.
  4. parse each query into a stable contract and clarify low-confidence cases.
  5. Add reranking, citations, and deterministic validation.
  6. Turn real failures into an evaluation set with weekly regression runs.
  7. Add knowledge graphs, personalization, or multi-agent orchestration only when the baseline reveals a concrete bottleneck.

Looking back at the 2022 design, my conclusion is not that the old algorithms became useless. It is that architecture decisions should outlive model names.

A trustworthy domain system knows where its knowledge came from, to whom it applies, and when it is valid. It understands the problem the user is trying to solve. And when the evidence is insufficient, it knows when to stop.

Those properties still matter more than the name of the model.

Further reading

39 / 41

My AI Programming Workbench: Orchestrating Models Instead of Hunting for One Best Tool

How I combine Ollama with Qwen3.5:9B, OpenCode, Claude Code, Codex, and desktop GPT through project context, architecture review, task decomposition, cross-review, and evidence-based quality gates.
Aug 06, 2026 · 11 min read

My programming toolbox is no longer a single product.

Locally, I run Ollama with Qwen3.5:9B. In the terminal I use OpenCode, Claude Code, and Codex, and I also route Kimi K3 through OpenCode. For complex desktop work, I use the desktop version of GPT. These tools differ in capability, cost, privacy, and interaction style, but I have become convinced that output quality depends less on finding one “best model” than on organizing several models well.

I do not want one agent to receive an ambiguous request and independently design, implement, and approve the result. My workflow resembles a small engineering team. I establish shared project context, ask a strong model to develop an architecture, and personally review the critical decisions. I then divide the work into bounded tasks, assign them to different tools, ask other tools to review the implementation, and close the loop with tests, type checks, static analysis, and observed behavior.

The point is not to open more terminals. It is to build an auditable delivery pipeline:

Requirements and constraints
          ↓
Project context (AGENTS.md / CLAUDE.md / documentation)
          ↓
Design (architecture, domain boundaries, risks, acceptance criteria)
          ↓
Human review and decision
          ↓
Task decomposition → execution → independent cross-review
          ↓
Automated checks + scenario validation
          ↓
Corrections become durable project context

A layered workbench, not a list of tools

Different models belong at different points in the workflow. Sending every task to the most expensive model wastes resources. Giving a high-risk design decision to a small local model can make the team move quickly in the wrong direction.

LayerTools I useBest suited to
Local explorationOllama + Qwen3.5:9BCode explanation, local search, drafts, low-risk edits, and assistance with sensitive context
Open orchestrationOpenCode + Qwen / Kimi K3Model switching, experiments, cost-capability comparisons, and replaceable terminal workflows
Deep designClaude CodeBroad repository comprehension, architecture proposals, domain modeling, and migration planning
Engineering executionCodex / Claude Code / OpenCodeRepository exploration, implementation, refactoring, testing, repair, and review
Desktop collaborationDesktop GPTCross-file and visual reasoning, research synthesis, long-running collaboration, and editorial refinement

This is not a permanent ranking. Models and products will change. The stable rule is to route work according to risk, context size, verifiability, cost, and privacy requirements.

Low-risk, reversible tasks should usually start with a local or lower-cost model. Cross-module reasoning deserves a stronger model. Architecture, data migration, security, and production changes require a human decision. No model output substitutes for acceptance evidence.

Start with project context, not a clever prompt

When I initialize a project, I create project-level instructions such as AGENTS.md and CLAUDE.md. They describe the stack, repository structure, operating commands, architecture, and engineering constraints. This preparation looks like documentation, but it establishes the ceiling for every agent that follows.

Without shared context, every tool rescans the repository, guesses conventions, and may invent a different implementation style. Useful project context should answer:

Purpose: the problem, users, and current stage
Stack: languages, frameworks, versions, and major dependencies
Architecture: module responsibilities, dependency direction, hard boundaries
Domain language: core concepts, terms, and invariants
Commands: install, start, test, type-check, and build
Conventions: naming, errors, logging, tests, and commits
Safety: sensitive data, secrets, external calls, and prohibited actions
Definition of done: mandatory quality gates for every change

Context files should not become thousand-line dumping grounds. AGENTS.md is a good home for stable, executable, cross-tool rules. CLAUDE.md can contain Claude-specific interaction guidance. Detailed architecture belongs in focused documents linked from the entry point. Rules should be concise, testable, and updated with the code.

One principle matters greatly: do not duplicate the same rule across several files. Duplication eventually becomes contradiction. Shared facts need one canonical source; tool-specific files should contain only the differences.

Let a strong model design, but keep architecture accountable to people

For medium and large changes, I often ask Claude to propose the architecture, domain boundaries, DDD model, and implementation path. AI is excellent at expanding the problem space quickly: identifying affected modules, proposing alternatives, finding failure paths, exposing hidden dependencies, and turning vague requirements into a structure that can be debated.

A polished design document is not automatically a correct design. I review five things:

  1. Does it solve the real problem? Or has it added complexity to display architectural sophistication?
  2. Are the boundaries natural? Do they follow business change, or merely apply DDD vocabulary?
  3. Are dependencies controlled? Are data flow, transactions, recovery, and compatibility explicit?
  4. Can it ship incrementally? Or does it require a high-risk big-bang rewrite?
  5. How will we know it works? Are test, performance, migration, and business acceptance criteria defined before implementation?

DDD is a way to organize complex business knowledge, not a decorative default. A short-lived module with straightforward CRUD does not need aggregates, repositories, and extra layers for their own sake. DDD earns its cost when rules are complex, language is contested, and boundaries must evolve over time.

I therefore ask the model to explain why the design fits, what alternatives exist, and when the recommendation should not be used. Good architecture is not an impressive diagram. It is a set of decisions that can be challenged, traded off, and verified.

Decompose work around boundaries and evidence

Once the design passes review, I do not send another agent a one-line request to “implement the whole feature.” I create task packets that are independently understandable and verifiable, with as little overlapping write scope as possible.

A useful task packet includes:

Objective: the user or system behavior that must change
Scope: permitted modules and explicit non-goals
Context: relevant decisions, interfaces, and conventions
Acceptance: tests, examples, performance, or observable behavior
Risk: compatibility, data, security, and rollback requirements
Delivery: code, tests, documentation, and unresolved questions

Tasks should be split by domain boundary, module, read/write responsibility, or verification concern—not mechanically by file. When multiple agents edit the same central file, time saved in generation is often lost to conflict resolution and context synchronization. Parallelism pays only when boundaries are clear and write scopes barely overlap.

I also control how much context each task receives. Giving an agent the entire repository is not always helpful. Irrelevant information dilutes constraints and increases accidental associations. Stable project rules should be shared, while each task receives only the local context necessary for its objective.

Cross-review: different tools should search for different failures

I ask other tools to cross-review the implementation. The value is not that two models can vote. It is that an independent reviewer can challenge the implementer's assumptions.

An effective review is more specific than “look over this code.” I ask reviewers to check:

  • conformance with the requirement and acceptance criteria;
  • architecture boundaries and hidden coupling;
  • error paths, concurrency, idempotency, and resource cleanup;
  • security, privacy, permissions, and dependency risk;
  • whether tests verify behavior instead of mirroring implementation;
  • whether a simpler and more maintainable solution exists.

The reviewer should provide evidence: a file and location, trigger condition, impact, reproduction path, and suggested correction. The implementation tool can then address the finding and rerun validation. When models disagree, I do not decide by brand. I return to requirements, code, tests, and reproducible facts.

There is also a danger of agents endorsing one another. If several tools inherit the same faulty assumption, they can confidently agree on the wrong result. Independent verification should change either the information source or the validation method: one model performs static review, another runs tests and scenarios, and a person reviews the critical business judgment.

The final judge is a quality gate, not a model

Code that looks plausible is not evidence that the system behaves correctly. My validation ladder usually looks like this:

Formatting / linting
        ↓
Type checking / compilation
        ↓
Unit and integration tests
        ↓
Critical user journey or API scenario checks
        ↓
Diff and architecture consistency review

Automatable checks belong in project commands and CI, not in an agent's memory. An agent must report which commands it actually ran, their results, and which checks could not run because of environmental limits. An unexecuted test is not “probably passing,” and a prediction is not evidence.

High-risk actions need separate governance. Production deployment, database migration, destructive changes, permission updates, and external communication require explicit approval and a recovery plan. Local inference can improve privacy, but it is not automatically secure. Model provenance, tool permissions, secrets in prompts, logs, and external plugins still require control.

Turn every correction into default capability

The lasting value of this workflow is not that one change ships faster. It is the learning loop.

When review exposes a recurring problem, I decide which layer should absorb it. Stable engineering constraints go into AGENTS.md. Architectural trade-offs become ADRs. Domain facts enter domain documentation. Mechanical errors become lint rules or tests. High-value failures become regression cases. The next task should not depend on me remembering the same warning again.

I can then track cycle time from request to merge, first-pass validation rate, review findings, human rework, rollback count, and the cost of comparable tasks across models. Generation speed alone is a poor evaluation metric. A fast model that creates hours of review work may be the slower system.

My conclusion: the developer becomes the designer of the work system

Ollama, Qwen, OpenCode, Kimi, Claude Code, Codex, and desktop GPT will all continue to evolve. Today's strongest model may become an ordinary component tomorrow.

What compounds is model-independent: clarifying the objective, encoding context in the project, turning architecture into reviewable decisions, decomposing work into verifiable units, assigning execution and review independently, and closing the loop with automated evidence.

AI lowers the cost of coding and exploration, but it does not take ownership of judgment or responsibility. My role is no longer limited to typing every line. It is to design an engineering system in which the right context enters, different capabilities cooperate, errors surface early, and experience becomes durable memory.

That may be the deepest change AI programming brings: we are no longer only designing software; we are also designing the way software gets produced.

40 / 41

Building Production Intelligent Risk Control: Streaming, Lakehouse, Rules, and Models

A production architecture for low-latency, replayable, auditable, and degradable risk decisions using Kafka, Spark Structured Streaming, CDC, Hadoop/Iceberg, rules, and models.
Aug 06, 2026 · 16 min read

The hardest part of risk control is not expressing a rule such as “five failed logins from one device in ten minutes.” It is making a stable, explainable, and traceable decision when events arrive out of order, messages repeat, services fail, strategies change, and traffic peaks at the same time.

A mature system must answer four questions:

  • Should this request proceed now? Payments, logins, and withdrawals often need a response in milliseconds.
  • What has happened recently? Has an account, device, or network shown a burst, cluster, or behavioral shift?
  • What does history tell us? Can offline data produce reliable baselines, labels, profiles, and training examples?
  • Why was this decision made? Can we reconstruct the events, features, rules, and model versions used?

Production risk control is therefore neither one Spark job nor one rule repository. It is a decision system composed of synchronous serving, asynchronous stream processing, an offline lakehouse, strategy operations, and a feedback loop.

Architecture: two paths, one fact history, full replay

Production real-time and offline risk architecture

The key separation is between synchronous interception and real-time computation:

  1. Synchronous online path: a Risk API reads online features, evaluates rules and models, and returns allow, deny, or review within a strict latency budget.
  2. Asynchronous streaming path: business events enter Kafka; Spark Structured Streaming performs event-time deduplication, sliding windows, stateful aggregation, and feature updates.
  3. Offline batch path: immutable facts land in a lakehouse on HDFS or object storage; Spark Batch handles replay, reconciliation, backfill, rule simulation, and model datasets.
  4. Unified strategy and audit: features, rules, models, and decision records are versioned so an online outcome can be reproduced offline.

This is safer than blocking every business request on Spark. Structured Streaming normally executes in micro-batches and is well suited to second-level state updates. A transaction requiring consistently low tens-of-milliseconds latency needs an independently deployed serving path so scheduler delays, backpressure, or checkpoint pauses cannot enter the critical request path.

Establish an immutable risk fact stream

Kafka should be treated as a durable risk event log, not temporary plumbing. Payments, logins, devices, account changes, list updates, and investigation outcomes become governed events.

Every event should contain at least:

event_id          globally unique identity for deduplication and tracing
event_type        business meaning and semantic version
event_time        when the business event actually occurred
ingest_time       when the platform received it
entity_keys       user_id / account_id / device_id / ip
payload           facts rather than transient computed judgments
trace_id          correlation across request, stream, and decision
schema_version    compatible evolution
source            producing system and environment

Design topics around business facts and retention requirements, not one topic per rule. A partition key should match the stateful computation: account_id preserves account order, while network detection may require device_id. Derive separately keyed streams rather than expecting one key to serve every use case.

Use idempotent producers, strong acknowledgements, and governed Avro, Protobuf, or JSON Schema. Consumers still require idempotency because end-to-end exactly-once behavior depends on the external sink. A practical system target is at-least-once transport + business-key deduplication + idempotent writes + reconciliation and replay.

Do not poll operational databases aggressively. Capture account state, lists, and merchant reference data through log-based CDC. When a database update and a domain event must agree, use a Transactional Outbox to avoid the dual-write gap between committing data and publishing a message.

Define sliding windows in event time

Risk windows must use event_time, not the time Spark happens to process a record. Mobile disconnection, network delay, and Kafka backlog cause disorder; processing-time logic can produce different outcomes for the same facts after a restart.

Common windows include:

1-minute window sliding every 10 seconds: transaction burst
10-minute window sliding every minute: failures and amount velocity
24-hour window sliding every 15 minutes: accounts per device
7-day state: new beneficiary, familiar geography, behavioral baseline

Window length expresses observation range; slide interval expresses update frequency. Smaller slides increase state and compute cost. Derive them from the maximum acceptable detection delay rather than setting every feature to one second.

A watermark says how long the system is willing to wait for late data and when it may clear state. It does not guarantee that all records arrive within that delay. Choose it from measured lateness—perhaps covering 99.9% of events—and route records beyond the threshold to a late-data path for backfill, audit, and monitoring rather than silently discarding them.

events = (
    spark.readStream.format("kafka")
    .option("subscribe", "risk.payment.v1")
    .load()
    .transform(parse_and_validate)
    .withWatermark("event_time", "10 minutes")
    .dropDuplicatesWithinWatermark(["event_id"])
)

velocity = events.groupBy(
    window("event_time", "10 minutes", "1 minute"),
    "account_id",
).agg(
    count("*").alias("tx_count_10m"),
    sum("amount").alias("tx_amount_10m"),
)

This illustrates semantics, not a copy-ready production job. Real pipelines also need schema validation, quarantine, state bounds, isolated checkpoint locations, rate control, monitoring, and idempotent sinks.

Streaming features must leave Spark memory

Window results, recent entity sets, velocity metrics, and risk counters belong in a low-latency online store read by the synchronous decision service. Redis, Cassandra, HBase, or an existing highly available KV store can work, but first define a feature contract:

feature_name + entity_key + value
event_time + computed_at
definition_version + producer_job_version
ttl + freshness_sla

Writes must be idempotent. With foreachBatch, use a query/batch identity or business window key, plus an upsert, transactional table, or commit log. Spark checkpoints can restore source offsets and computation state; they do not automatically make an arbitrary external database exactly-once.

Define stale-feature behavior explicitly. When the store is unavailable or freshness exceeds its SLA, the engine must distinguish a true zero from missing data and choose a conservative rule set, fallback model, manual review, or constrained approval according to risk.

Rules and models are complementary

A mature decision combines several mechanisms:

Hard rules: regulation, deny lists, unambiguous prohibitions
Velocity rules: counts, amounts, linked entities, time windows
Model scores: fraud, anomaly, account-takeover probability
Policy orchestration: matches + scores + business cost → action

Rules fit deterministic, explainable, rapidly changing constraints. Models fit multivariate patterns that are difficult to hand-code. The final action should incorporate exposure, customer value, false-positive cost, and available review capacity—not only one score.

A rule platform needs version, state, priority, audience, effective interval, author, approver, and rationale. The release path should be draft → tests → historical replay → shadow → canary → full rollout. Record the rule version and input snapshot for every match, but avoid synchronous heavy audit writes in the high-QPS path; publish a reliable decision event and persist it asynchronously.

Keep the rule DSL constrained and analyzable. Arbitrary scripts should not access the network or query production databases per transaction. Reference data should arrive through governed interfaces or preloaded snapshots.

Modernize the Hadoop layer into a governed lakehouse

HDFS remains appropriate for distributed storage in on-premises Hadoop deployments; cloud systems commonly use object storage. In either case, bare Parquet directories are a weak sole data-management layer. An open table format such as Iceberg adds atomic commits, schema and partition evolution, snapshots, and time travel—strong foundations for replay and audit.

Use at least three layers:

  • Raw facts: append-only Kafka and CDC archives with original schema versions.
  • Conformed detail: deduplication, master-data mapping, privacy treatment, and consistent time semantics.
  • Features and labels: datasets for rule replay, training, cases, and analytics.

Spark Batch performs reconciliation, historical windows, feature backfills, fraud labels, and strategy evaluation. Batch and streaming should share feature definitions or be generated from the same declarative logic. Training datasets require point-in-time joins that use only information available at decision time, preventing future leakage.

Streaming into Iceberg creates frequent snapshots and small files. Run separate maintenance for compaction, manifest rewriting, snapshot expiration, and metadata monitoring. Retention is also a product decision: raw facts, features, decisions, and sensitive identifiers need distinct retention and deletion policies.

Every decision must be replayable

Persist a Decision Record containing:

decision_id / request_id / trace_id
event_id and raw-fact location
feature values, event times, and definition versions
matched rules and versions
model name, version, score, and threshold
final action, reason code, and human override
decision time, latency, and degradation state

Audit supports compliance and operations. It lets the team explain why a customer passed yesterday and failed today, whether a rule caused false positives, and whether model deterioration came from drift, stale features, or a business distribution shift.

Use stable reason codes. Operational explanations and customer-facing explanations should be designed separately so staff can act on them without revealing exploitable strategy details.

Availability requires explicit degradation semantics

Production design defines behavior under failure:

FailureRecommended behavior
Kafka backlogContinue with timestamped recent features; monitor freshness and activate conservative policy
Online store unavailableUse local cache and core rules; send high-risk requests to review or deny
Model timeoutCircuit-break quickly; fall back to rules and the last stable model
Bad rule releaseRoll back immediately; require dual approval and shadowing for high-impact rules
Streaming job failureRecover from durable checkpoints; replay from Kafka and the archived facts
Data-quality incidentQuarantine bad events, freeze affected features, alert ownership, and stop propagation

Deploy the decision service across failure domains and assign an end-to-end latency budget plus dependency timeouts. Kafka, checkpoints, online stores, and lakehouse each need RPO/RTO. Disaster recovery is not “the process restarted”; periodically prove recovery from offsets, checkpoints, and immutable history.

Observe systems, data, and decisions

CPU and latency are insufficient. Operate three metric families:

  • System: QPS, p95/p99 latency, errors, Kafka lag, batch duration, state size, checkpoint failures.
  • Data: volume, schema failures, duplicates, lateness, nulls, feature freshness, online/offline consistency.
  • Risk: allow/deny/review rate, rule hits, score distribution, false positives, fraud loss, queue depth, policy value.

Alerts should describe impact. Rising lag matters because particular features crossed their SLA and a number of decisions entered degradation—not simply because a threshold turned red.

A practical delivery sequence

1. Facts and the minimum decision loop

  • Standardize event schemas, IDs, event time, and reason codes.
  • Ingest one critical event through Kafka and archive it unchanged.
  • Deploy a decision API with a small hard-rule set and complete Decision Records.
  • Establish baselines for latency, errors, lag, and decision distribution.

2. Real-time state

  • Implement two or three high-value windows in Structured Streaming.
  • Add watermarks, late-data routing, deduplication, and idempotent sinks.
  • Introduce an online feature store and freshness SLAs.
  • Exercise restart, backlog, and online-store degradation.

3. Rule operations and replay

  • Add rule versioning, approval, shadowing, canary, and rollback.
  • Build Iceberg fact, feature, label, and decision tables.
  • Backtest strategies and validate online/offline feature consistency.
  • Close the loop with investigator outcomes.

4. Models and continuous optimization

  • Build leakage-free datasets with point-in-time joins.
  • Add model registry, explanation, shadowing, canary, and drift monitoring.
  • Optimize thresholds across loss, false positives, and operating cost.
  • Review overlapping rules, unused features, technical debt, and recovery readiness.

The standard for production maturity

A production risk system should demonstrate that:

  1. synchronous latency is bounded and streaming cannot stall the transaction path;
  2. Kafka holds replayable facts, with explicit duplicate, disorder, and lateness semantics;
  3. online and offline features share definitions, while rules and models are versioned;
  4. every decision is explainable, auditable, and reproducible;
  5. each dependency has a deliberate degradation policy;
  6. success is measured through loss prevented, false-positive cost, and customer experience.

Risk control is not the pursuit of maximum rejection. It is the discipline of making cost-aware, evidence-based, accountable decisions from changing information under time pressure. Kafka, Spark, Hadoop, and a rule engine are infrastructure. The real system is the learning loop that joins live facts, historical evidence, and business judgment.

Further reading

41 / 41

From Random Weights to Local Inference

How I implemented and trained a decoder-only Transformer on a 16GB MacBook Air, then learned why tokenization, data quality, evaluation, and inference controls matter more than a low validation loss.
Aug 06, 2026 · 14 min read

The easiest way to train a Transformer is to hide most of the process behind a framework. The easiest things to miss are how text becomes tokens, how gradients change random weights, how checkpoints become a reloadable model, and why excellent validation loss can coexist with poor answers to real questions.

I built Local Transformer Training Lab to run the entire chain on a personal computer:

Dataset generation
→ Tokenizer
→ Decoder-only Transformer
→ Pretraining
→ Supervised fine-tuning
→ Independent evaluation
→ Checkpoints and export
→ Local inference

The experiments ran on a MacBook Air with 16GB of memory, using Apple MLX and the Metal GPU. Every model began with random weights. None was a fine-tune of Qwen, Llama, or another pretrained model.

The evolution of three local Transformer experiments

Why train from scratch?

Model APIs are excellent for building products, but they hide several questions that matter when a system fails:

  • How does tokenization change sequence length and output safety?
  • What does a causal attention mask actually look like in code?
  • Why do pretraining and SFT need different data and loss regions?
  • Why can a model with low validation loss fail after a simple paraphrase?
  • Where should repetition, invalid text, and empty answers be handled?
  • What must be saved to reload a model reliably in another process?

The value of training from scratch is not the parameter count. It is that no anomaly can be dismissed as “the black box.” The relationship between data, architecture, optimization, and inference becomes concrete.

Generation zero: make the complete loop work

The first programmer-world-tiny dataset contained roughly 10 MiB of locally generated material covering Go, Linux, Redis, MySQL, Docker, code completion, and engineering interviews.

ItemValue
Records23,377
Train / validation22,908 / 469
Exact text duplication0%
External API useNone

The model was intentionally small: four decoder blocks, four attention heads, a hidden width of 128, and an FFN width of 384. The 128-token version had 742,272 parameters; extending context to 512 tokens increased it to 791,424.

The implementation in src/model.py contains the complete path:

Token embedding + position embedding
→ RMSNorm
→ causal multi-head self-attention
→ residual connection
→ RMSNorm
→ GELU feed-forward network
→ residual connection
→ language-model head

The causal mask prevents position t from seeing future tokens. Next-token training shifts one sequence by a single position: the model receives every token except the last and predicts every token except the first.

Training uses AdamW, gradient clipping, periodic validation, and separate best, checkpoint, and final exports. A reloadable package contains three essential files:

config.json
model.safetensors
tokenizer.json

Weights without configuration cannot reconstruct the network. A model without the exact tokenizer cannot interpret its token IDs.

Byte tokenization: simple, universal, and expensive

The first tokenizer represented UTF-8 bytes directly. Its vocabulary contained 263 entries: 256 byte values plus special markers. It required no tokenizer training, was reversible, and covered Chinese, English, and code.

The cost was sequence length. A Chinese character usually consumes three byte tokens. A nominal 512-token context therefore holds far fewer than 512 Chinese characters. A model can also generate an incomplete UTF-8 byte sequence, producing the replacement character at the terminal.

After 2,500 steps, the 512-token model reached a best validation loss of 0.135433 and a final perplexity of 1.1514 in about 110.63 seconds. Those numbers looked impressive. Its real answers did not.

The train and validation records came from the same template distribution. The low loss primarily proved that the model had learned those regularities—not that it possessed general programming knowledge.

Generation one: BPE, two training phases, and answer-only loss

v1 expanded the model to 8,534,272 parameters: eight layers, eight heads, a hidden width of 256, and a 4,096-token ByteLevel BPE vocabulary.

The efficiency difference was immediate:

“Why is Redis so fast?” (Chinese prompt)
Byte tokenizer: 28 tokens
BPE tokenizer: 7 tokens

The more important change was separating training into two phases.

Domain pretraining

Pretraining material was transformed into technical notes and complete code rather than question-answer wrappers. The objective was to learn domain language, code structure, and next-token regularities.

Supervised fine-tuning

SFT data removed synthetic phrases such as “additional constraint” and “simulated business context,” then applied semantic deduplication and category balancing. Loss was computed only after the first <|assistant|> marker, so the model optimized the answer rather than spending most of its capacity reproducing user text and control tokens.

v1 completed 800 pretraining steps and 1,500 SFT steps. Its best SFT validation loss was 0.235517. Independent paraphrased prompts exposed a different reality: Redis, MySQL, and Docker answers could bleed into one another; identity answers picked up interview templates; paraphrases generalized poorly; and some prompts produced an immediate end marker.

The useful conclusion was not merely that loss improved:

Scale, BPE, and training technique improve representation and fit. Natural variation, supervision quality, and independent evaluation determine whether the model can actually answer.

Generation two: guarantee valid output first

ByteLevel BPE still allowed v1 to produce byte fragments that did not form valid UTF-8. Instead of blindly scaling the model again, v2 switched to a Unicode character tokenizer.

The corpus contained 750 distinct characters plus nine special tokens, for a vocabulary of 759. Every emitted token represents a valid Unicode code point. Unknown characters map explicitly to <|unk|>, and generation no longer produces .

v2 has 6,825,728 parameters, eight layers, eight heads, and a 512-token context. It completed 800 pretraining steps and 1,500 SFT steps in 115.45 seconds.

Character tokenization is not simply “better” than BPE. Sequences are longer, and without a KV cache generation is slower. The experiment separated three objectives that are often collapsed into one metric: model capability, tokenizer efficiency, and output validity.

Inference is a product layer

A script that can load weights is not yet a reliable inference interface. v2 added:

  • a repetition penalty over the most recent 64 tokens;
  • stopping after four identical consecutive tokens;
  • stopping after the same trigram repeats three times;
  • truncation when control markers leak into the answer;
  • removal of Unicode replacement characters;
  • an explicit fallback for empty answers;
  • normalization of common short prompts;
  • --no-history to prevent untrained multi-turn context from compounding errors.

Disabling history by default may be the least intuitive decision. A 512-token window does not mean the model learned multi-turn dialogue. Without reliable multi-turn SFT, feeding one wrong answer into the next prompt makes errors compound. When a capability is not ready, doing less is more trustworthy than pretending to support it.

Evaluating a teaching model honestly

A random split of one template distribution produces optimistic metrics. The project eventually used three layers of checks:

  1. Implementation correctness: tokenizer round trips, mask behavior, shapes, save/reload, and minimal training tests.
  2. In-distribution metrics: training loss, validation loss, perplexity, and gradient norm.
  3. Behavior outside the template: human paraphrases, domain leakage, empty answers, repetition, invalid text, and multi-turn contamination.

The most valuable artifacts are often the failures, not the best number. Each failure points toward the layer that needs work: data, tokenizer, loss, architecture, or inference policy.

Five lessons I am keeping

1. Data structure matters more than raw size

Ten MiB of highly templated data can produce a low loss and weak generalization. Deduplication must go beyond string equality and prevent semantic templates from leaking across train and validation sets.

2. The tokenizer is part of the architecture

It determines vocabulary parameters, sequence length, context utilization, unknown-character behavior, and output validity. It is not a replaceable preprocessing detail.

3. Pretraining and SFT solve different problems

Pretraining learns language and domain distributions. SFT teaches the model how to respond within a task protocol. Mixing both into one undifferentiated next-token dataset obscures those roles.

4. Loss answers only the question it was designed to answer

In-distribution validation loss asks whether the model predicts similar tokens. It does not ask whether the model understands natural user language. Metrics must correspond to the capability being claimed.

5. Inference controls do not hide model weakness

Stopping repetition, falling back safely, and disabling history do not make the model smarter. They make its boundary honest, stable, and observable. Reliable systems must limit how errors propagate, not only maximize correct outputs.

What comes next

The project now completes the journey from random weights to local inference, but it remains a teaching model. The next valuable steps are not more synthetic templates or blindly increasing training steps. They are:

  • licensed, source-traceable Chinese technical and code data;
  • 1,000–5,000 human-written questions, paraphrases, and follow-ups;
  • semantic topic isolation across train, validation, and test;
  • compiler tests for code and rule-based or human scoring for technical answers;
  • controlled comparisons across 10M, 30M, and 50M parameters;
  • a KV cache and explicit speed-memory-quality evaluation.

Training from scratch did not make modern language models look less impressive. It made the hidden coordination behind every good answer visible: data, tokenization, architecture, optimization, evaluation, and inference policy all have to work together.

Repository and full experiment reports: hh696-wq/local-transformer-training-lab